Revert "CF-1041 observability v2 " need more changes and testing (#2375)
Reverts codeflash-ai/codeflash-internal#2329
This commit is contained in:
parent
07d33edd9f
commit
98fb2d1579
60 changed files with 4620 additions and 4069 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -163,7 +163,6 @@ cython_debug/
|
|||
#.idea/
|
||||
.aider*
|
||||
.serena/
|
||||
.planning/
|
||||
/js/common/node_modules/
|
||||
/node_modules/
|
||||
*.xml
|
||||
|
|
|
|||
3
.vscode/settings.json
vendored
3
.vscode/settings.json
vendored
|
|
@ -15,8 +15,5 @@
|
|||
],
|
||||
"[python]": {
|
||||
"editor.defaultFormatter": "charliermarsh.ruff"
|
||||
},
|
||||
"[typescriptreact]": {
|
||||
"editor.defaultFormatter": "vscode.typescript-language-features"
|
||||
}
|
||||
}
|
||||
|
|
@ -23,8 +23,8 @@
|
|||
|
||||
**CRITICAL: IMPORT PATH RULES**:
|
||||
- **NEVER add file extensions (.js, .ts, .tsx) to import paths** - The test framework resolves extensions automatically.
|
||||
- **WRONG**: `import {{ fn }} from '../utils.js'` or `import {{ fn }} from '../utils.ts'`
|
||||
- **CORRECT**: `import {{ fn }} from '../utils'`
|
||||
- **WRONG**: `import {{fn}} from '../utils.js'` or `import {{fn}} from '../utils.ts'`
|
||||
- **CORRECT**: `import {{fn}} from '../utils'`
|
||||
- The user message provides the exact import statement to use - copy it exactly without modification.
|
||||
|
||||
**CRITICAL: VITEST IMPORTS REQUIRED**:
|
||||
|
|
|
|||
|
|
@ -241,30 +241,8 @@ async def generate_and_validate_test_code(
|
|||
call_sequence: int | None = None,
|
||||
function_to_optimize: FunctionToOptimize | None = None,
|
||||
module_path: str | None = None,
|
||||
test_module_path: str | None = None,
|
||||
helper_function_names: list[str] | None = None,
|
||||
is_async: bool = False,
|
||||
) -> str:
|
||||
obs_context: dict | None = (
|
||||
{
|
||||
"call_sequence": call_sequence,
|
||||
"module_path": module_path,
|
||||
"test_module_path": test_module_path,
|
||||
"helper_function_names": helper_function_names,
|
||||
"is_async": is_async,
|
||||
"function_to_optimize": {
|
||||
"function_name": function_to_optimize.function_name,
|
||||
"file_path": function_to_optimize.file_path,
|
||||
"qualified_name": function_to_optimize.qualified_name,
|
||||
"starting_line": function_to_optimize.starting_line,
|
||||
"ending_line": function_to_optimize.ending_line,
|
||||
}
|
||||
if function_to_optimize is not None
|
||||
else None,
|
||||
}
|
||||
if call_sequence is not None
|
||||
else None
|
||||
)
|
||||
obs_context: dict | None = {"call_sequence": call_sequence} if call_sequence is not None else None
|
||||
response = await call_llm(
|
||||
llm=model,
|
||||
messages=messages,
|
||||
|
|
@ -340,9 +318,6 @@ async def generate_regression_tests_from_function(
|
|||
call_sequence=call_sequence,
|
||||
function_to_optimize=data.function_to_optimize,
|
||||
module_path=data.module_path,
|
||||
test_module_path=data.test_module_path,
|
||||
helper_function_names=data.helper_function_names,
|
||||
is_async=data.function_to_optimize.is_async or data.is_async or False,
|
||||
)
|
||||
total_llm_cost = sum(cost_tracker)
|
||||
await update_optimization_cost(trace_id=trace_id, cost=total_llm_cost, user_id=user_id)
|
||||
|
|
|
|||
6
js/cf-webapp/.gitignore
vendored
6
js/cf-webapp/.gitignore
vendored
|
|
@ -42,8 +42,4 @@ next-env.d.ts
|
|||
/.npmrc
|
||||
.npmrc
|
||||
/.azure/config
|
||||
*.next/*
|
||||
|
||||
# Generated WASM files (built by postinstall)
|
||||
/public/web-tree-sitter.wasm
|
||||
/public/tree-sitter-python.wasm
|
||||
*.next/*
|
||||
|
|
@ -1,21 +1,11 @@
|
|||
/** @type {import("next").NextConfig} */
|
||||
let nextConfig = {
|
||||
transpilePackages: ["@codeflash-ai/common"],
|
||||
webpack: (config, { isServer }) => {
|
||||
webpack: (config) => {
|
||||
config.watchOptions = {
|
||||
poll: 1000,
|
||||
aggregateTimeout: 300,
|
||||
}
|
||||
// Handle web-tree-sitter's Node.js module imports in browser
|
||||
if (!isServer) {
|
||||
config.resolve.fallback = {
|
||||
...config.resolve.fallback,
|
||||
fs: false,
|
||||
"fs/promises": false,
|
||||
path: false,
|
||||
module: false,
|
||||
}
|
||||
}
|
||||
return config
|
||||
},
|
||||
experimental: {
|
||||
|
|
|
|||
760
js/cf-webapp/package-lock.json
generated
760
js/cf-webapp/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -14,7 +14,6 @@
|
|||
"prisma:generate": "npx prisma generate",
|
||||
"prisma:migrate": "npx prisma migrate dev",
|
||||
"prepare": "simple-git-hooks",
|
||||
"postinstall": "cp node_modules/web-tree-sitter/web-tree-sitter.wasm public/ && npx tree-sitter build --wasm node_modules/tree-sitter-python -o public/tree-sitter-python.wasm",
|
||||
"format": "prettier --write \"**/*.{js,ts,tsx,json,md}\"",
|
||||
"format:check": "prettier --check \"**/*.{js,ts,tsx,json,md}\""
|
||||
},
|
||||
|
|
@ -25,8 +24,6 @@
|
|||
"@hookform/resolvers": "^3.3.2",
|
||||
"@monaco-editor/react": "^4.7.0",
|
||||
"@prisma/client": "^6.7.0",
|
||||
"@radix-ui/react-accordion": "^1.2.12",
|
||||
"@radix-ui/react-collapsible": "^1.1.12",
|
||||
"@radix-ui/react-dialog": "^1.0.5",
|
||||
"@radix-ui/react-dropdown-menu": "^2.0.6",
|
||||
"@radix-ui/react-label": "^2.0.2",
|
||||
|
|
@ -47,7 +44,6 @@
|
|||
"chart.js": "^4.4.9",
|
||||
"class-variance-authority": "^0.7.0",
|
||||
"clsx": "^2.0.0",
|
||||
"cmdk": "^1.1.1",
|
||||
"date-fns": "^4.1.0",
|
||||
"diff": "^8.0.2",
|
||||
"framer-motion": "^12.12.1",
|
||||
|
|
@ -66,7 +62,6 @@
|
|||
"prism-react-renderer": "^2.4.1",
|
||||
"react": "^18",
|
||||
"react-chartjs-2": "^5.3.0",
|
||||
"react-diff-viewer-continued": "^3.4.0",
|
||||
"react-dom": "^18",
|
||||
"react-hook-form": "^7.48.2",
|
||||
"react-markdown": "^9.0.1",
|
||||
|
|
@ -74,13 +69,10 @@
|
|||
"react-syntax-highlighter": "^16.1.0",
|
||||
"remark-gfm": "^4.0.0",
|
||||
"sharp": "^0.34.2",
|
||||
"shiki": "^3.21.0",
|
||||
"sonner": "^2.0.6",
|
||||
"tailwind-merge": "^2.0.0",
|
||||
"tailwindcss": "^3.3.0",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"vaul": "^1.1.2",
|
||||
"web-tree-sitter": "^0.26.3",
|
||||
"zod": "^3.22.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
|
@ -100,12 +92,9 @@
|
|||
"eslint-plugin-react": "^7.33.2",
|
||||
"jsdom": "^24.1.0",
|
||||
"lint-staged": "^15.4.3",
|
||||
"postcss-import": "^16.1.1",
|
||||
"prettier": "3.2.5",
|
||||
"prisma": "^6.7.0",
|
||||
"simple-git-hooks": "^2.9.0",
|
||||
"tree-sitter-cli": "^0.26.3",
|
||||
"tree-sitter-python": "^0.25.0",
|
||||
"typescript": "^5.4.5",
|
||||
"vitest": "^3.0.8"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@
|
|||
import LogoBox from "@/components/dashboard/logo-box"
|
||||
import { useState, useEffect } from "react"
|
||||
import { useRouter, useSearchParams } from "next/navigation"
|
||||
import Image from "next/image"
|
||||
import { Loading } from "@/components/ui/loading"
|
||||
import {
|
||||
authorizeOAuth,
|
||||
|
|
@ -288,11 +287,9 @@ export default function CodeFlashAuthContent() {
|
|||
}`}
|
||||
>
|
||||
{userInfo?.avatarUrl ? (
|
||||
<Image
|
||||
<img
|
||||
src={userInfo.avatarUrl}
|
||||
alt={userInfo.name}
|
||||
width={32}
|
||||
height={32}
|
||||
className="w-8 h-8 rounded-full"
|
||||
/>
|
||||
) : (
|
||||
|
|
@ -330,7 +327,7 @@ export default function CodeFlashAuthContent() {
|
|||
}`}
|
||||
>
|
||||
{org.avatarUrl ? (
|
||||
<Image src={org.avatarUrl} alt={org.name} width={32} height={32} className="w-8 h-8 rounded-md" />
|
||||
<img src={org.avatarUrl} alt={org.name} className="w-8 h-8 rounded-md" />
|
||||
) : (
|
||||
<div className="w-8 h-8 rounded-md bg-gradient-to-br from-orange-500 to-red-600 flex items-center justify-center text-white text-xs font-medium">
|
||||
{getInitials(org.name)}
|
||||
|
|
|
|||
|
|
@ -22,7 +22,6 @@ import React from "react"
|
|||
import { generateToken } from "./tokenfuncs"
|
||||
import { Plus, User, Building2, Check } from "lucide-react"
|
||||
import { useViewMode } from "@/app/app/ViewModeContext"
|
||||
import Image from "next/image"
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
|
|
@ -176,11 +175,9 @@ export function CreateApiKeyDialog(): React.JSX.Element {
|
|||
<SelectItem key={org.id} value={org.id}>
|
||||
<div className="flex items-center gap-2">
|
||||
{org.avatarUrl ? (
|
||||
<Image
|
||||
<img
|
||||
src={org.avatarUrl}
|
||||
alt={org.name}
|
||||
width={20}
|
||||
height={20}
|
||||
className="h-5 w-5 rounded-full"
|
||||
/>
|
||||
) : (
|
||||
|
|
|
|||
|
|
@ -287,7 +287,6 @@ export default function OptimizationReviewPage() {
|
|||
}
|
||||
}
|
||||
loadEvent()
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [params.traceId, currentOrg?.id])
|
||||
|
||||
const loadComments = async (eventId: string) => {
|
||||
|
|
@ -604,9 +603,9 @@ export default function OptimizationReviewPage() {
|
|||
window.open(constructedUrl, "_blank")
|
||||
}
|
||||
}, 1000)
|
||||
} catch (error: unknown) {
|
||||
} catch (error: any) {
|
||||
console.error("[handleCreatePR] Exception:", error)
|
||||
const errorMessage = error instanceof Error ? error.message : "Failed to create pull request"
|
||||
const errorMessage = error?.message || "Failed to create pull request"
|
||||
toast.error(errorMessage, {
|
||||
duration: 5000,
|
||||
})
|
||||
|
|
@ -683,7 +682,7 @@ export default function OptimizationReviewPage() {
|
|||
)}
|
||||
</h1>
|
||||
{event.speedup_x && (
|
||||
<span className="flex items-center gap-2 rounded-sm bg-zinc-900 border border-zinc-800 px-3 py-1 text-xs font-bold text-zinc-50">
|
||||
<span className="flex items-center gap-2 rounded-md bg-gradient-to-r from-primary to-yellow-500 px-3 py-1 text-xs font-bold text-gray-900">
|
||||
<svg className="h-3.5 w-3.5" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M13 10V3L4 14h7v7l9-11h-7z" />
|
||||
</svg>
|
||||
|
|
|
|||
|
|
@ -122,7 +122,6 @@ export default function LineProfilerPage() {
|
|||
}
|
||||
|
||||
loadEvent()
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [params.traceId, currentOrg?.id])
|
||||
|
||||
const handleBack = () => {
|
||||
|
|
@ -205,7 +204,7 @@ export default function LineProfilerPage() {
|
|||
)}
|
||||
</h1>
|
||||
{event.speedup_x && (
|
||||
<span className="flex items-center gap-2 rounded-sm bg-zinc-900 border border-zinc-800 px-3 py-1 text-xs font-bold text-zinc-50">
|
||||
<span className="flex items-center gap-2 rounded-md bg-gradient-to-r from-primary to-yellow-500 px-3 py-1 text-xs font-bold text-gray-900">
|
||||
<svg className="h-3.5 w-3.5" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M13 10V3L4 14h7v7l9-11h-7z" />
|
||||
</svg>
|
||||
|
|
|
|||
|
|
@ -203,7 +203,6 @@ function Dashboard() {
|
|||
setLoading(false)
|
||||
fetchingRef.current = false
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [selectedYear, currentOrgId])
|
||||
|
||||
useEffect(() => {
|
||||
|
|
|
|||
|
|
@ -2,18 +2,84 @@
|
|||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
/* Import the design token system */
|
||||
@import "../styles/tokens.css";
|
||||
@import "../styles/typography.css";
|
||||
@import "../styles/spacing.css";
|
||||
|
||||
@layer base {
|
||||
/* Light mode removed - dark mode only implementation */
|
||||
:root {
|
||||
/* Background and foreground */
|
||||
--background: 0 0% 99%;
|
||||
--foreground: 222.2 84% 4.9%;
|
||||
|
||||
/* Card styles */
|
||||
--card: 0 0% 100%;
|
||||
--card-foreground: 222.2 84% 4.9%;
|
||||
|
||||
/* Popover styles */
|
||||
--popover: 0 0% 100%;
|
||||
--popover-foreground: 222.2 84% 4.9%;
|
||||
|
||||
/* Codeflash primary colors - converted from hex to HSL */
|
||||
--primary: 38 100% 63%; /* #d08e0d - Codeflash yellow */
|
||||
--primary-foreground: 0 6% 4%;
|
||||
|
||||
/* Secondary colors - complementary to Codeflash yellow */
|
||||
--secondary: 41 88% 95%; /* Lighter version of primary */
|
||||
--secondary-foreground: 41 88% 20%; /* Darker version for contrast */
|
||||
|
||||
/* Accent colors - variation of the Codeflash yellow */
|
||||
--accent: 41 70% 90%; /* Softer version of primary */
|
||||
--accent-foreground: 41 88% 20%;
|
||||
|
||||
/* Other UI colors aligned with brand */
|
||||
--muted: 41 20% 96%;
|
||||
--muted-foreground: 41 8% 46%;
|
||||
|
||||
--destructive: 0 84.2% 60.2%;
|
||||
--destructive-foreground: 0 0% 100%;
|
||||
|
||||
--border: 41 30% 90%;
|
||||
--input: 41 30% 90%;
|
||||
--ring: 38 100% 63%; /* Matching primary - Codeflash yellow */
|
||||
|
||||
--radius: 0.5rem;
|
||||
|
||||
/* Code highlighting */
|
||||
--docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.dark {
|
||||
/* All color tokens are defined in tokens.css */
|
||||
/* The .dark class enables Tailwind's dark: variant */
|
||||
/* Since we're dark mode only, tokens are already set for dark mode */
|
||||
/* Background and foreground */
|
||||
--background: 0, 6%, 5%;
|
||||
--foreground: 0 0% 100%;
|
||||
|
||||
/* Card styles */
|
||||
--card: 0 3% 11%;
|
||||
--card-foreground: 0 0% 100%;
|
||||
|
||||
/* Popover styles */
|
||||
--popover: 222.2 84% 4.9%;
|
||||
--popover-foreground: 0 0% 100%;
|
||||
|
||||
/* Codeflash primary colors for dark mode */
|
||||
--primary: 38 100% 63%; /* #ffd227 - Codeflash yellow for dark mode */
|
||||
--primary-foreground: 222.2 47.4% 11.2%;
|
||||
|
||||
/* Secondary colors - complementary to Codeflash yellow in dark mode */
|
||||
--secondary: 48 60% 25%; /* Darker version of primary */
|
||||
--secondary-foreground: 48 100% 80%; /* Lighter version for contrast */
|
||||
|
||||
/* Accent colors - variation of the Codeflash yellow */
|
||||
--accent: 48 70% 30%; /* Softer version of primary */
|
||||
--accent-foreground: 48 100% 80%;
|
||||
|
||||
/* Other UI colors aligned with brand */
|
||||
--muted: 48 15% 20%;
|
||||
--muted-foreground: 48 20% 65%;
|
||||
|
||||
--destructive: 0 62.8% 30.6%;
|
||||
--destructive-foreground: 0 0% 100%;
|
||||
|
||||
--border: 48 20% 25%;
|
||||
--input: 48 20% 25%;
|
||||
--ring: 38 100% 63%; /* Matching primary - Codeflash yellow */
|
||||
|
||||
/* Code highlighting */
|
||||
--docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.3);
|
||||
|
|
@ -22,8 +88,8 @@
|
|||
|
||||
@layer base {
|
||||
::selection {
|
||||
background: rgb(113 113 122); /* zinc-500 - no brand colors */
|
||||
color: rgb(250 250 250); /* zinc-50 for contrast */
|
||||
background: #ffd227; /* CF brand color */
|
||||
color: #1f2937; /* Tailwind's gray-800 for selected text color */
|
||||
}
|
||||
|
||||
* {
|
||||
|
|
@ -80,11 +146,11 @@
|
|||
}
|
||||
|
||||
.prose code {
|
||||
background-color: rgb(var(--muted));
|
||||
background-color: hsl(var(--muted));
|
||||
padding: 0.125em 0.25em;
|
||||
border-radius: 0.25em;
|
||||
font-size: 0.875em;
|
||||
font-family: var(--font-mono);
|
||||
font-family: ui-monospace, monospace;
|
||||
}
|
||||
|
||||
.prose pre {
|
||||
|
|
@ -98,10 +164,10 @@
|
|||
}
|
||||
|
||||
.prose blockquote {
|
||||
border-left: 3px solid rgb(var(--border));
|
||||
border-left: 3px solid hsl(var(--border));
|
||||
padding-left: 1em;
|
||||
margin-left: 0;
|
||||
color: rgb(var(--muted-foreground));
|
||||
color: hsl(var(--muted-foreground));
|
||||
}
|
||||
|
||||
/* Fixed list styles to show bullets and numbers */
|
||||
|
|
@ -138,7 +204,7 @@
|
|||
}
|
||||
|
||||
.prose a {
|
||||
color: rgb(var(--primary));
|
||||
color: hsl(var(--primary));
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
|
|
@ -173,42 +239,10 @@
|
|||
|
||||
/* Dark mode adjustments */
|
||||
.dark .prose code {
|
||||
background-color: rgb(var(--muted));
|
||||
background-color: hsl(var(--muted));
|
||||
}
|
||||
|
||||
.dark .prose blockquote {
|
||||
border-left-color: rgb(var(--border));
|
||||
color: rgb(var(--muted-foreground));
|
||||
}
|
||||
|
||||
/* Typography utility classes for common patterns */
|
||||
@layer utilities {
|
||||
/* Apply monospace font for inline code */
|
||||
.text-code {
|
||||
font-family: var(--font-mono);
|
||||
font-size: var(--text-sm);
|
||||
}
|
||||
|
||||
/* Apply monospace font with tight line height for data */
|
||||
.text-data {
|
||||
font-family: var(--font-mono);
|
||||
line-height: var(--leading-tight);
|
||||
}
|
||||
|
||||
/* Apply sans font with medium weight for UI labels */
|
||||
.text-label {
|
||||
font-family: var(--font-sans);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* Standard container padding using spacing tokens */
|
||||
.container-spacing {
|
||||
padding-left: var(--space-4);
|
||||
padding-right: var(--space-4);
|
||||
}
|
||||
|
||||
/* Standard card internal spacing */
|
||||
.card-spacing {
|
||||
padding: var(--space-3);
|
||||
}
|
||||
border-left-color: hsl(var(--border));
|
||||
color: hsl(var(--muted-foreground));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import type { Metadata } from "next"
|
||||
import { Inter as FontSans, JetBrains_Mono } from "next/font/google"
|
||||
import { Inter as FontSans } from "next/font/google"
|
||||
import "./globals.css"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { ThemeProvider } from "@/components/theme-provider"
|
||||
|
|
@ -23,13 +23,6 @@ const fontSans = FontSans({
|
|||
variable: "--font-sans",
|
||||
})
|
||||
|
||||
const jetbrainsMono = JetBrains_Mono({
|
||||
subsets: ["latin"],
|
||||
weight: ["300", "400", "500", "600", "700"],
|
||||
variable: "--font-jetbrains-mono",
|
||||
display: "swap",
|
||||
})
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Codeflash",
|
||||
description: "Optimize the performance of your code.",
|
||||
|
|
@ -98,7 +91,7 @@ export default async function RootLayout({
|
|||
}}
|
||||
/>
|
||||
</head>
|
||||
<body className={cn("min-h-screen bg-background font-sans antialiased", fontSans.variable, jetbrainsMono.variable)}>
|
||||
<body className={cn("min-h-screen bg-background font-sans antialiased", fontSans.variable)}>
|
||||
<PostHogPageView />
|
||||
<UserProvider>
|
||||
<ThemeProvider
|
||||
|
|
|
|||
|
|
@ -1,18 +1,11 @@
|
|||
import { ReactNode } from "react"
|
||||
import { ObservabilityNav } from "@/components/observability/observability-nav"
|
||||
|
||||
export default function Observability2Layout({ children }: { children: ReactNode }) {
|
||||
export default function ObservabilityLayout({ children }: { children: ReactNode }) {
|
||||
return (
|
||||
<div className="min-h-screen bg-zinc-50 dark:bg-zinc-950">
|
||||
<nav className="border-b border-zinc-200 dark:border-zinc-700 bg-white dark:bg-zinc-800">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="flex h-14 items-center justify-center">
|
||||
<h1 className="text-lg font-semibold text-zinc-900 dark:text-zinc-50">
|
||||
Observability v2
|
||||
</h1>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
<main className="flex-1">{children}</main>
|
||||
<div className="min-h-screen bg-gray-50 dark:bg-gray-900">
|
||||
<ObservabilityNav />
|
||||
<div className="flex-1">{children}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
524
js/cf-webapp/src/app/observability/llm-call/[id]/page.tsx
Normal file
524
js/cf-webapp/src/app/observability/llm-call/[id]/page.tsx
Normal file
|
|
@ -0,0 +1,524 @@
|
|||
import Link from "next/link"
|
||||
import { Metadata } from "next"
|
||||
import { notFound } from "next/navigation"
|
||||
import {
|
||||
CheckCircle,
|
||||
XCircle,
|
||||
Hash,
|
||||
FileText,
|
||||
Code,
|
||||
AlertTriangle,
|
||||
} from "lucide-react"
|
||||
import { prisma } from "@/lib/prisma"
|
||||
import { StatCard } from "@/components/observability/stat-card"
|
||||
import { InfoIcon } from "@/components/observability/info-icon"
|
||||
import { CopyButton } from "@/components/observability/copy-button"
|
||||
import { ParsedResponseView } from "@/components/observability/parsed-response-view"
|
||||
|
||||
interface LLMCallDetailPageProps {
|
||||
params: {
|
||||
id: string
|
||||
}
|
||||
}
|
||||
|
||||
export async function generateMetadata({ params }: LLMCallDetailPageProps): Promise<Metadata> {
|
||||
return {
|
||||
title: `LLM Call ${params.id.substring(0, 8)} - Observability`,
|
||||
description: "View LLM call details for prompt engineering analysis",
|
||||
}
|
||||
}
|
||||
|
||||
export default async function LLMCallDetailPage({ params }: LLMCallDetailPageProps) {
|
||||
// Fetch LLM call details
|
||||
const llmCall = await prisma.llm_calls.findUnique({
|
||||
where: { id: params.id },
|
||||
})
|
||||
|
||||
if (!llmCall) {
|
||||
notFound()
|
||||
}
|
||||
|
||||
// Fetch related errors
|
||||
const relatedErrors = await prisma.optimization_errors.findMany({
|
||||
where: { llm_call_id: params.id },
|
||||
orderBy: { created_at: "desc" },
|
||||
})
|
||||
|
||||
return (
|
||||
<div className="container mx-auto px-4 py-8">
|
||||
{/* Header */}
|
||||
<div className="mb-8">
|
||||
{/* Breadcrumb */}
|
||||
<div className="flex items-center gap-2 text-sm text-gray-600 dark:text-gray-400 mb-4">
|
||||
<Link
|
||||
href="/observability/llm-calls"
|
||||
className="hover:text-blue-600 dark:hover:text-blue-400"
|
||||
>
|
||||
LLM Calls
|
||||
</Link>
|
||||
<span>/</span>
|
||||
<span className="text-gray-900 dark:text-white font-mono">
|
||||
{llmCall.id.substring(0, 8)}...
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<h1 className="text-3xl font-bold tracking-tight text-gray-900 dark:text-white mb-3">
|
||||
LLM Call Detail
|
||||
</h1>
|
||||
|
||||
{/* ID and Trace with Copy Buttons */}
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm text-gray-600 dark:text-gray-400">Call ID:</span>
|
||||
<code className="text-sm font-mono text-gray-900 dark:text-white bg-gray-100 dark:bg-gray-700 px-2 py-1 rounded">
|
||||
{llmCall.id}
|
||||
</code>
|
||||
<CopyButton text={llmCall.id} label="call ID" size="sm" />
|
||||
</div>
|
||||
{llmCall.trace_id && (
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm text-gray-600 dark:text-gray-400">Trace:</span>
|
||||
<Link
|
||||
href={`/observability/trace/${llmCall.trace_id}`}
|
||||
className="text-sm font-mono text-blue-600 dark:text-blue-400 hover:underline bg-gray-100 dark:bg-gray-700 px-2 py-1 rounded"
|
||||
>
|
||||
{llmCall.trace_id}
|
||||
</Link>
|
||||
<CopyButton text={llmCall.trace_id} label="trace ID" size="sm" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Summary Cards */}
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-5 mb-8">
|
||||
<StatCard
|
||||
label="Status"
|
||||
value={llmCall.status}
|
||||
helpText="Call outcome: success, failed, partial_success, or in_progress"
|
||||
icon={
|
||||
llmCall.status === "success"
|
||||
? "CheckCircle2"
|
||||
: llmCall.status === "failed"
|
||||
? "AlertTriangle"
|
||||
: "Clock"
|
||||
}
|
||||
variant={
|
||||
llmCall.status === "success"
|
||||
? "success"
|
||||
: llmCall.status === "failed"
|
||||
? "error"
|
||||
: "warning"
|
||||
}
|
||||
/>
|
||||
<StatCard
|
||||
label="Latency"
|
||||
value={llmCall.latency_ms ? `${llmCall.latency_ms}ms` : "N/A"}
|
||||
helpText="API response time in milliseconds. Time from request to completion."
|
||||
icon="Clock"
|
||||
/>
|
||||
<StatCard
|
||||
label="Tokens"
|
||||
value={llmCall.total_tokens?.toLocaleString() ?? "N/A"}
|
||||
helpText="Total tokens (prompt + completion) used in this call"
|
||||
icon="Database"
|
||||
/>
|
||||
<StatCard
|
||||
label="Cost"
|
||||
value={llmCall.llm_cost ? `$${llmCall.llm_cost.toFixed(4)}` : "N/A"}
|
||||
helpText="Cost in USD for this specific call based on model pricing"
|
||||
icon="DollarSign"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Metadata */}
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg shadow p-6 mb-8 border border-gray-200 dark:border-gray-700">
|
||||
<h2 className="text-xl font-semibold mb-4 text-gray-900 dark:text-white">Metadata</h2>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div className="flex items-start gap-2">
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-1.5 mb-1">
|
||||
<span className="text-sm font-medium text-gray-600 dark:text-gray-400">
|
||||
Call Type
|
||||
</span>
|
||||
<InfoIcon content="Purpose of this LLM operation (optimization, validation, line_profiler, etc.)" side="top" />
|
||||
</div>
|
||||
<span className="font-semibold text-gray-900 dark:text-gray-100">
|
||||
{llmCall.call_type}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-start gap-2">
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-1.5 mb-1">
|
||||
<span className="text-sm font-medium text-gray-600 dark:text-gray-400">Model</span>
|
||||
<InfoIcon content="AI model and version used for this call" side="top" />
|
||||
</div>
|
||||
<span className="font-semibold text-gray-900 dark:text-gray-100">
|
||||
{llmCall.model_name}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-start gap-2">
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-1.5 mb-1">
|
||||
<span className="text-sm font-medium text-gray-600 dark:text-gray-400">
|
||||
Temperature
|
||||
</span>
|
||||
<InfoIcon content="Randomness setting (0=deterministic, 1=creative). Controls output variability." side="top" />
|
||||
</div>
|
||||
<span className="font-semibold text-gray-900 dark:text-gray-100">
|
||||
{llmCall.temperature || "default"}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-start gap-2">
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-1.5 mb-1">
|
||||
<span className="text-sm font-medium text-gray-600 dark:text-gray-400">
|
||||
Candidates Requested
|
||||
</span>
|
||||
<InfoIcon content="Number of code variations requested from the model" side="top" />
|
||||
</div>
|
||||
<span className="font-semibold text-gray-900 dark:text-gray-100">
|
||||
{llmCall.n_candidates || "N/A"}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-start gap-2">
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-1.5 mb-1">
|
||||
<span className="text-sm font-medium text-gray-600 dark:text-gray-400">
|
||||
Created
|
||||
</span>
|
||||
<InfoIcon content="When this call was initiated" side="top" />
|
||||
</div>
|
||||
<span className="font-semibold text-gray-900 dark:text-gray-100">
|
||||
{new Date(llmCall.created_at).toLocaleString()}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-start gap-2">
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-1.5 mb-1">
|
||||
<span className="text-sm font-medium text-gray-600 dark:text-gray-400">
|
||||
Parsing Status
|
||||
</span>
|
||||
<InfoIcon content="Whether model output was successfully parsed into usable code candidates" side="top" />
|
||||
</div>
|
||||
<span
|
||||
className={`font-semibold ${
|
||||
llmCall.parsing_status === "success"
|
||||
? "text-green-600 dark:text-green-400"
|
||||
: "text-red-600 dark:text-red-400"
|
||||
}`}
|
||||
>
|
||||
{llmCall.parsing_status || "N/A"}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Token Breakdown */}
|
||||
{llmCall.prompt_tokens && (
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg shadow p-6 mb-8 border border-gray-200 dark:border-gray-700">
|
||||
<div className="flex items-center gap-2 mb-4">
|
||||
<Hash className="h-5 w-5 text-gray-600 dark:text-gray-400" />
|
||||
<h2 className="text-xl font-semibold text-gray-900 dark:text-white">Token Usage</h2>
|
||||
</div>
|
||||
|
||||
{/* Visual Token Ratio Bar */}
|
||||
<div className="mb-6">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<span className="text-sm font-medium text-gray-600 dark:text-gray-400">
|
||||
Token Distribution
|
||||
</span>
|
||||
<InfoIcon content="Visual breakdown of prompt vs completion tokens" side="top" />
|
||||
</div>
|
||||
<div className="flex h-8 rounded-lg overflow-hidden border border-gray-200 dark:border-gray-600">
|
||||
{(() => {
|
||||
const promptTokens = llmCall.prompt_tokens ?? 0
|
||||
const completionTokens = llmCall.completion_tokens ?? 0
|
||||
const totalTokens = llmCall.total_tokens ?? (promptTokens + completionTokens)
|
||||
// Use 1 as fallback only for division to prevent division by zero
|
||||
const safeTotalTokens = totalTokens || 1
|
||||
const promptPercent = Math.round((promptTokens / safeTotalTokens) * 100)
|
||||
const completionPercent = Math.round((completionTokens / safeTotalTokens) * 100)
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className="bg-blue-500 dark:bg-blue-600 flex items-center justify-center text-white text-xs font-semibold"
|
||||
style={{ width: `${promptPercent}%` }}
|
||||
>
|
||||
{promptPercent > 0 && <span className="px-2">{promptPercent}%</span>}
|
||||
</div>
|
||||
<div
|
||||
className="bg-green-500 dark:bg-green-600 flex items-center justify-center text-white text-xs font-semibold"
|
||||
style={{ width: `${completionPercent}%` }}
|
||||
>
|
||||
{completionPercent > 0 && <span className="px-2">{completionPercent}%</span>}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
})()}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-3 gap-6">
|
||||
{(() => {
|
||||
const promptTokens = llmCall.prompt_tokens ?? 0
|
||||
const completionTokens = llmCall.completion_tokens ?? 0
|
||||
const totalTokens = llmCall.total_tokens ?? (promptTokens + completionTokens)
|
||||
|
||||
return (
|
||||
<>
|
||||
<div>
|
||||
<div className="flex items-center gap-1.5 mb-2">
|
||||
<div className="w-3 h-3 rounded-sm bg-blue-500 dark:bg-blue-600"></div>
|
||||
<span className="text-sm font-medium text-gray-600 dark:text-gray-400">
|
||||
Prompt Tokens
|
||||
</span>
|
||||
</div>
|
||||
<div className="text-2xl font-bold text-gray-900 dark:text-white">
|
||||
{promptTokens.toLocaleString()}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="flex items-center gap-1.5 mb-2">
|
||||
<div className="w-3 h-3 rounded-sm bg-green-500 dark:bg-green-600"></div>
|
||||
<span className="text-sm font-medium text-gray-600 dark:text-gray-400">
|
||||
Completion Tokens
|
||||
</span>
|
||||
</div>
|
||||
<div className="text-2xl font-bold text-gray-900 dark:text-white">
|
||||
{completionTokens.toLocaleString()}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="flex items-center gap-1.5 mb-2">
|
||||
<span className="text-sm font-medium text-gray-600 dark:text-gray-400">
|
||||
Total Tokens
|
||||
</span>
|
||||
</div>
|
||||
<div className="text-2xl font-bold text-gray-900 dark:text-white">
|
||||
{totalTokens.toLocaleString()}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
})()}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Parsing Results */}
|
||||
{llmCall.candidates_generated !== null && (
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg shadow p-6 mb-8 border border-gray-200 dark:border-gray-700">
|
||||
<div className="flex items-center gap-2 mb-4">
|
||||
<Code className="h-5 w-5 text-gray-600 dark:text-gray-400" />
|
||||
<h2 className="text-xl font-semibold text-gray-900 dark:text-white">Parsing Results</h2>
|
||||
<InfoIcon content="Results from parsing the model's response into code candidates" side="top" />
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-6">
|
||||
<div>
|
||||
<div className="flex items-center gap-1.5 mb-2">
|
||||
<span className="text-sm font-medium text-gray-600 dark:text-gray-400">
|
||||
Candidates Generated
|
||||
</span>
|
||||
</div>
|
||||
<div className="text-2xl font-bold text-gray-900 dark:text-white">
|
||||
{llmCall.candidates_generated}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="flex items-center gap-1.5 mb-2">
|
||||
<CheckCircle className="h-4 w-4 text-green-600 dark:text-green-400" />
|
||||
<span className="text-sm font-medium text-gray-600 dark:text-gray-400">
|
||||
Candidates Valid
|
||||
</span>
|
||||
</div>
|
||||
<div className="text-2xl font-bold text-green-600 dark:text-green-400">
|
||||
{llmCall.candidates_valid}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{llmCall.parsing_errors && (
|
||||
<div className="mt-6 p-4 bg-red-50 dark:bg-red-900/20 rounded-lg border border-red-200 dark:border-red-800">
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<XCircle className="h-4 w-4 text-red-600 dark:text-red-400" />
|
||||
<div className="text-sm font-semibold text-red-800 dark:text-red-300">
|
||||
Parsing Errors
|
||||
</div>
|
||||
</div>
|
||||
<pre className="bg-white dark:bg-gray-900 p-3 rounded-lg text-xs overflow-auto text-gray-900 dark:text-gray-100 border border-red-200 dark:border-red-800 font-mono">
|
||||
{JSON.stringify(llmCall.parsing_errors, null, 2)}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Prompts */}
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg shadow p-6 mb-8 border border-gray-200 dark:border-gray-700">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<FileText className="h-5 w-5 text-gray-600 dark:text-gray-400" />
|
||||
<h2 className="text-xl font-semibold text-gray-900 dark:text-white">System Prompt</h2>
|
||||
<InfoIcon content="Instructions that guide the model's behavior throughout the conversation" side="top" />
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xs text-gray-500 dark:text-gray-400">
|
||||
{llmCall.system_prompt?.length.toLocaleString() || 0} characters
|
||||
</span>
|
||||
<CopyButton text={llmCall.system_prompt || ""} label="system prompt" size="sm" />
|
||||
</div>
|
||||
</div>
|
||||
<pre className="bg-gray-50 dark:bg-gray-900 p-4 rounded-lg text-sm overflow-auto whitespace-pre-wrap text-gray-900 dark:text-gray-100 leading-relaxed font-mono border border-gray-200 dark:border-gray-700">
|
||||
{llmCall.system_prompt}
|
||||
</pre>
|
||||
</div>
|
||||
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg shadow p-6 mb-8 border border-gray-200 dark:border-gray-700">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<FileText className="h-5 w-5 text-gray-600 dark:text-gray-400" />
|
||||
<h2 className="text-xl font-semibold text-gray-900 dark:text-white">User Prompt</h2>
|
||||
<InfoIcon content="Specific task and context for this call. Contains the code to optimize and requirements." side="top" />
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xs text-gray-500 dark:text-gray-400">
|
||||
{llmCall.user_prompt?.length.toLocaleString() || 0} characters
|
||||
</span>
|
||||
<CopyButton text={llmCall.user_prompt || ""} label="user prompt" size="sm" />
|
||||
</div>
|
||||
</div>
|
||||
<pre className="bg-gray-50 dark:bg-gray-900 p-4 rounded-lg text-sm overflow-auto whitespace-pre-wrap text-gray-900 dark:text-gray-100 leading-relaxed font-mono border border-gray-200 dark:border-gray-700">
|
||||
{llmCall.user_prompt}
|
||||
</pre>
|
||||
</div>
|
||||
|
||||
{/* Response — parsed by call type (ranking: rank/explain; optimization: code blocks + text), with View raw */}
|
||||
{llmCall.raw_response && (
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg shadow p-6 mb-8 border border-gray-200 dark:border-gray-700">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<Code className="h-5 w-5 text-gray-600 dark:text-gray-400" />
|
||||
<h2 className="text-xl font-semibold text-gray-900 dark:text-white">LLM Response</h2>
|
||||
<InfoIcon content="Parsed view by call type: ranking shows order and explanation; optimization shows code blocks. Use View raw for the full string." side="top" />
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xs text-gray-500 dark:text-gray-400">
|
||||
{llmCall.raw_response.length.toLocaleString()} characters
|
||||
</span>
|
||||
<CopyButton text={llmCall.raw_response} label="response" size="sm" />
|
||||
</div>
|
||||
</div>
|
||||
<ParsedResponseView
|
||||
rawResponse={llmCall.raw_response}
|
||||
callType={llmCall.call_type}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Error Information */}
|
||||
{llmCall.status === "failed" && llmCall.error_message && (
|
||||
<div className="bg-red-50 dark:bg-red-900/20 rounded-lg shadow p-6 mb-8 border-l-4 border-red-500 dark:border-red-600">
|
||||
<div className="flex items-start gap-3 mb-4">
|
||||
<XCircle className="h-6 w-6 text-red-600 dark:text-red-400 mt-0.5 flex-shrink-0" />
|
||||
<h2 className="text-xl font-semibold text-red-800 dark:text-red-400">
|
||||
Error Information
|
||||
</h2>
|
||||
</div>
|
||||
<div className="mb-4">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<span className="text-sm font-medium text-gray-700 dark:text-gray-300">Error Type:</span>
|
||||
<span className="px-2 py-1 bg-red-100 dark:bg-red-900 text-red-800 dark:text-red-300 rounded text-sm font-semibold">
|
||||
{llmCall.error_type}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<span className="text-sm font-medium text-gray-700 dark:text-gray-300">Error Message:</span>
|
||||
<CopyButton text={llmCall.error_message} label="error message" size="sm" />
|
||||
</div>
|
||||
<pre className="bg-white dark:bg-gray-900 p-3 rounded-lg text-sm overflow-auto text-red-700 dark:text-red-400 border border-red-200 dark:border-red-800 font-mono leading-relaxed">
|
||||
{llmCall.error_message}
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Related Errors */}
|
||||
{relatedErrors.length > 0 && (
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg shadow p-6 mb-8 border border-gray-200 dark:border-gray-700">
|
||||
<div className="flex items-center gap-3 mb-4">
|
||||
<AlertTriangle className="h-5 w-5 text-orange-600 dark:text-orange-400" />
|
||||
<h2 className="text-xl font-semibold text-gray-900 dark:text-white">Related Errors</h2>
|
||||
<span className="bg-red-100 dark:bg-red-900 text-red-800 dark:text-red-200 text-sm px-2.5 py-0.5 rounded font-semibold">
|
||||
{relatedErrors.length}
|
||||
</span>
|
||||
</div>
|
||||
<div className="space-y-4">
|
||||
{relatedErrors.map(error => (
|
||||
<div
|
||||
key={error.id}
|
||||
className="border-l-4 border-red-500 dark:border-red-600 pl-4 py-2 bg-gray-50 dark:bg-gray-900/50 rounded-r-lg"
|
||||
>
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<span
|
||||
className={`px-2 py-1 rounded text-xs font-semibold ${
|
||||
error.severity === "critical"
|
||||
? "bg-red-100 dark:bg-red-900 text-red-800 dark:text-red-200"
|
||||
: error.severity === "error"
|
||||
? "bg-orange-100 dark:bg-orange-900 text-orange-800 dark:text-orange-200"
|
||||
: "bg-yellow-100 dark:bg-yellow-900 text-yellow-800 dark:text-yellow-200"
|
||||
}`}
|
||||
>
|
||||
{error.severity}
|
||||
</span>
|
||||
<span className="text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
{error.error_type}
|
||||
</span>
|
||||
<span className="text-xs text-gray-500 dark:text-gray-400 ml-auto">
|
||||
{new Date(error.created_at).toLocaleString()}
|
||||
</span>
|
||||
</div>
|
||||
<div className="text-sm mb-2 text-gray-900 dark:text-gray-100 font-medium">
|
||||
{error.error_message}
|
||||
</div>
|
||||
{error.context && (
|
||||
<details className="text-sm text-gray-600 dark:text-gray-400">
|
||||
<summary className="cursor-pointer hover:text-blue-600 dark:hover:text-blue-400">
|
||||
View context
|
||||
</summary>
|
||||
<pre className="bg-white dark:bg-gray-900 p-3 rounded-lg mt-2 overflow-auto text-gray-900 dark:text-gray-100 text-xs border border-gray-200 dark:border-gray-700 font-mono">
|
||||
{JSON.stringify(error.context, null, 2)}
|
||||
</pre>
|
||||
</details>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{/* Actions */}
|
||||
<div className="flex flex-wrap gap-4">
|
||||
<Link
|
||||
href={`/observability/trace/${llmCall.trace_id}`}
|
||||
className="inline-flex items-center px-6 py-3 bg-blue-600 dark:bg-blue-700 text-white rounded-lg hover:bg-blue-700 dark:hover:bg-blue-600 transition-colors font-medium shadow-sm"
|
||||
>
|
||||
View Full Trace →
|
||||
</Link>
|
||||
<Link
|
||||
href="/observability/llm-calls"
|
||||
className="inline-flex items-center px-6 py-3 border border-gray-300 dark:border-gray-600 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 text-gray-900 dark:text-gray-100 transition-colors font-medium"
|
||||
>
|
||||
← Back to List
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
800
js/cf-webapp/src/app/observability/llm-calls/page.tsx
Normal file
800
js/cf-webapp/src/app/observability/llm-calls/page.tsx
Normal file
|
|
@ -0,0 +1,800 @@
|
|||
import Link from "next/link"
|
||||
import { Metadata } from "next"
|
||||
import { unstable_cache } from "next/cache"
|
||||
import { Award, Database as DatabaseIcon, Github, Terminal } from "lucide-react"
|
||||
import { prisma } from "@/lib/prisma"
|
||||
import { getCallSource } from "@/lib/observability-utils"
|
||||
import { HelpButton } from "@/components/observability/help-button"
|
||||
import { StatCard } from "@/components/observability/stat-card"
|
||||
import { ColumnHeader } from "@/components/observability/column-header"
|
||||
import { InfoIcon } from "@/components/observability/info-icon"
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "LLM Calls - Observability",
|
||||
description: "View all LLM API calls for prompt engineering analysis",
|
||||
}
|
||||
|
||||
interface SearchParams {
|
||||
call_type?: string
|
||||
model?: string
|
||||
status?: string
|
||||
trace_id?: string
|
||||
page?: string
|
||||
organization?: string
|
||||
}
|
||||
|
||||
// Cached function to get unique organizations list
|
||||
// Revalidates every 5 minutes - organizations change infrequently
|
||||
const getUniqueOrganizations = unstable_cache(
|
||||
async () => {
|
||||
const allOrganizations = await prisma.optimization_features.findMany({
|
||||
select: { organization: true },
|
||||
distinct: ["organization"],
|
||||
where: { organization: { not: null } },
|
||||
})
|
||||
return allOrganizations
|
||||
.map(f => f.organization)
|
||||
.filter(Boolean)
|
||||
.sort() as string[]
|
||||
},
|
||||
["unique-organizations"],
|
||||
{ revalidate: 300 }, // 5 minutes
|
||||
)
|
||||
|
||||
// Cached function to get unique call types
|
||||
// Revalidates every 5 minutes - call types change infrequently
|
||||
const getCallTypes = unstable_cache(
|
||||
async () => {
|
||||
const callTypes = await prisma.llm_calls.findMany({
|
||||
select: { call_type: true },
|
||||
distinct: ["call_type"],
|
||||
})
|
||||
return callTypes.filter(ct => ct.call_type !== null)
|
||||
},
|
||||
["call-types"],
|
||||
{ revalidate: 300 }, // 5 minutes
|
||||
)
|
||||
|
||||
// Cached function to get unique model names
|
||||
// Revalidates every 5 minutes - models change infrequently
|
||||
const getModels = unstable_cache(
|
||||
async () => {
|
||||
const models = await prisma.llm_calls.findMany({
|
||||
select: { model_name: true },
|
||||
distinct: ["model_name"],
|
||||
})
|
||||
return models.filter(m => m.model_name !== null)
|
||||
},
|
||||
["model-names"],
|
||||
{ revalidate: 300 }, // 5 minutes
|
||||
)
|
||||
|
||||
export default async function LLMCallsPage({ searchParams }: { searchParams: SearchParams }) {
|
||||
try {
|
||||
const page = parseInt(searchParams.page || "1")
|
||||
const pageSize = 50
|
||||
const skip = (page - 1) * pageSize
|
||||
|
||||
// Build where clause based on filters
|
||||
type WhereClause = {
|
||||
call_type?: string
|
||||
model_name?: { contains: string }
|
||||
status?: string
|
||||
trace_id?: { startsWith: string } | { in: string[] } | { contains: string }
|
||||
OR?: Array<{ trace_id: { startsWith: string } }>
|
||||
}
|
||||
|
||||
const where: WhereClause = {}
|
||||
|
||||
if (searchParams.call_type) {
|
||||
where.call_type = searchParams.call_type
|
||||
}
|
||||
if (searchParams.model) {
|
||||
where.model_name = { contains: searchParams.model }
|
||||
}
|
||||
if (searchParams.status) {
|
||||
where.status = searchParams.status
|
||||
}
|
||||
if (searchParams.trace_id) {
|
||||
// Use startsWith for prefix matching to find multi-model related calls
|
||||
where.trace_id = { startsWith: searchParams.trace_id }
|
||||
}
|
||||
|
||||
// Get unique organizations for filter dropdown (cached)
|
||||
const uniqueOrganizations = await getUniqueOrganizations()
|
||||
|
||||
// If organization filter is specified, get matching trace_ids
|
||||
let filteredTraceIds: string[] = []
|
||||
if (searchParams.organization) {
|
||||
const orgFeatures = await prisma.optimization_features.findMany({
|
||||
where: { organization: searchParams.organization },
|
||||
select: { trace_id: true },
|
||||
distinct: ["trace_id"],
|
||||
})
|
||||
filteredTraceIds = orgFeatures.map(f => f.trace_id).filter(Boolean) as string[]
|
||||
|
||||
// If organization filter is applied but no traces found, return empty result early
|
||||
if (filteredTraceIds.length === 0) {
|
||||
// Get unique call types and models for filters (cached)
|
||||
const [callTypes, models] = await Promise.all([
|
||||
getCallTypes(),
|
||||
getModels(),
|
||||
])
|
||||
|
||||
// Return early with empty results
|
||||
return (
|
||||
<div className="container mx-auto px-4 py-8">
|
||||
<div className="mb-6">
|
||||
{/* Title and Search Bar on Same Line */}
|
||||
<div className="flex items-center justify-between gap-4 mb-2">
|
||||
<h1 className="text-3xl font-bold text-gray-900 dark:text-white whitespace-nowrap">
|
||||
LLM Calls
|
||||
</h1>
|
||||
{/* Compact Search Bar */}
|
||||
<form method="get" className="flex items-center gap-2 flex-1 max-w-xl">
|
||||
<input
|
||||
type="text"
|
||||
name="trace_id"
|
||||
placeholder="Search by Trace ID..."
|
||||
defaultValue={searchParams.trace_id || ""}
|
||||
className="flex-1 px-3 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 placeholder-gray-500 dark:placeholder-gray-400"
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
className="px-4 py-2 text-sm bg-blue-600 dark:bg-blue-700 text-white rounded hover:bg-blue-700 dark:hover:bg-blue-600 whitespace-nowrap"
|
||||
>
|
||||
Search
|
||||
</button>
|
||||
{searchParams.trace_id && (
|
||||
<Link
|
||||
href="/observability/llm-calls"
|
||||
className="px-4 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded hover:bg-gray-50 dark:hover:bg-gray-700 text-gray-900 dark:text-gray-100 whitespace-nowrap"
|
||||
>
|
||||
Clear
|
||||
</Link>
|
||||
)}
|
||||
</form>
|
||||
</div>
|
||||
<p className="text-gray-600 dark:text-gray-400">
|
||||
Track and analyze all LLM API calls for prompt engineering
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Filters */}
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg shadow p-4 mb-6 border border-gray-200 dark:border-gray-700">
|
||||
<form method="get" className="flex flex-wrap gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-1 text-gray-700 dark:text-gray-300">
|
||||
Call Type
|
||||
</label>
|
||||
<select
|
||||
name="call_type"
|
||||
className="px-3 py-2 border border-gray-300 dark:border-gray-600 rounded bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100"
|
||||
defaultValue={searchParams.call_type || ""}
|
||||
>
|
||||
<option value="">All</option>
|
||||
{callTypes.map(ct => (
|
||||
<option key={ct.call_type} value={ct.call_type}>
|
||||
{ct.call_type}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-1 text-gray-700 dark:text-gray-300">
|
||||
Model
|
||||
</label>
|
||||
<select
|
||||
name="model"
|
||||
className="px-3 py-2 border border-gray-300 dark:border-gray-600 rounded bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100"
|
||||
defaultValue={searchParams.model || ""}
|
||||
>
|
||||
<option value="">All</option>
|
||||
{models.map(m => (
|
||||
<option key={m.model_name} value={m.model_name}>
|
||||
{m.model_name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-1 text-gray-700 dark:text-gray-300">
|
||||
Status
|
||||
</label>
|
||||
<select
|
||||
name="status"
|
||||
className="px-3 py-2 border border-gray-300 dark:border-gray-600 rounded bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100"
|
||||
defaultValue={searchParams.status || ""}
|
||||
>
|
||||
<option value="">All</option>
|
||||
<option value="success">Success</option>
|
||||
<option value="partial_success">Partial</option>
|
||||
<option value="failed">Failed</option>
|
||||
<option value="in_progress">In Progress</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-1 text-gray-700 dark:text-gray-300">
|
||||
Organization
|
||||
</label>
|
||||
<select
|
||||
name="organization"
|
||||
className="px-3 py-2 border border-gray-300 dark:border-gray-600 rounded bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100"
|
||||
defaultValue={searchParams.organization || ""}
|
||||
>
|
||||
<option value="">All Organizations</option>
|
||||
{uniqueOrganizations.map(org => (
|
||||
<option key={org} value={org}>
|
||||
{org}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="flex items-end">
|
||||
<button
|
||||
type="submit"
|
||||
className="px-4 py-2 bg-blue-600 dark:bg-blue-700 text-white rounded hover:bg-blue-700 dark:hover:bg-blue-600"
|
||||
>
|
||||
Filter
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg shadow p-12 text-center border border-gray-200 dark:border-gray-700">
|
||||
<p className="text-gray-500 dark:text-gray-400">
|
||||
No LLM calls found for organization "{searchParams.organization}".
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Apply organization filter using IN clause for exact trace ID matches
|
||||
// NOTE: Filtering happens at DB level BEFORE pagination, not client-side.
|
||||
// We use IN clause because there's no Prisma relation between llm_calls and optimization_features
|
||||
// (they're only related by trace_id as a string field, not a foreign key relation)
|
||||
if (filteredTraceIds.length > 0 && !searchParams.trace_id) {
|
||||
// Use IN clause for exact trace ID matches - much more efficient than OR with startsWith
|
||||
// For very large organizations (>10k traces), consider chunking the array
|
||||
where.trace_id = { in: filteredTraceIds }
|
||||
}
|
||||
|
||||
// Fetch LLM calls with pagination, aggregate stats
|
||||
const [llmCalls, totalCount, aggregateStats, successCount] = await Promise.all([
|
||||
prisma.llm_calls.findMany({
|
||||
where,
|
||||
orderBy: { created_at: "desc" },
|
||||
take: pageSize,
|
||||
skip,
|
||||
select: {
|
||||
id: true,
|
||||
trace_id: true,
|
||||
call_type: true,
|
||||
model_name: true,
|
||||
status: true,
|
||||
parsing_status: true,
|
||||
candidates_generated: true,
|
||||
candidates_valid: true,
|
||||
prompt_tokens: true,
|
||||
completion_tokens: true,
|
||||
llm_cost: true,
|
||||
latency_ms: true,
|
||||
created_at: true,
|
||||
error_message: true,
|
||||
context: true,
|
||||
},
|
||||
}),
|
||||
prisma.llm_calls.count({ where }),
|
||||
// Get aggregate stats for all filtered data (not just current page)
|
||||
prisma.llm_calls.aggregate({
|
||||
where,
|
||||
_sum: {
|
||||
llm_cost: true,
|
||||
latency_ms: true,
|
||||
},
|
||||
_avg: {
|
||||
latency_ms: true,
|
||||
},
|
||||
_count: {
|
||||
status: true,
|
||||
},
|
||||
}),
|
||||
// Get success count for success rate calculation
|
||||
prisma.llm_calls.count({
|
||||
where: {
|
||||
...where,
|
||||
status: "success",
|
||||
},
|
||||
}),
|
||||
])
|
||||
|
||||
// Fetch optimization_features and optimization_events for the trace_ids we got
|
||||
const traceIds = llmCalls.map(call => call.trace_id).filter(Boolean) as string[]
|
||||
const uniqueTraceIdPrefixes = Array.from(
|
||||
new Set(traceIds.map(id => id.substring(0, 36))), // Get base UUID (first 36 chars)
|
||||
)
|
||||
|
||||
// NOTE: Using Promise.all for parallel fetching, not N+1 queries.
|
||||
// Both queries execute simultaneously for the same set of trace_ids.
|
||||
const [optimizationFeatures, optimizationEvents] = await Promise.all([
|
||||
uniqueTraceIdPrefixes.length > 0
|
||||
? prisma.optimization_features.findMany({
|
||||
where: { trace_id: { in: uniqueTraceIdPrefixes } },
|
||||
select: {
|
||||
trace_id: true,
|
||||
organization: true,
|
||||
ranking: true,
|
||||
},
|
||||
})
|
||||
: [],
|
||||
uniqueTraceIdPrefixes.length > 0
|
||||
? prisma.optimization_events.findMany({
|
||||
where: { trace_id: { in: uniqueTraceIdPrefixes } },
|
||||
select: {
|
||||
trace_id: true,
|
||||
event_type: true,
|
||||
},
|
||||
distinct: ["trace_id"],
|
||||
})
|
||||
: [],
|
||||
])
|
||||
|
||||
// Create maps for trace_id to event_type and organization
|
||||
const traceIdToEventType = new Map<string, string>()
|
||||
optimizationEvents.forEach(event => {
|
||||
if (event.trace_id) {
|
||||
traceIdToEventType.set(event.trace_id, event.event_type)
|
||||
}
|
||||
})
|
||||
|
||||
const traceIdToOrganization = new Map<string, string>()
|
||||
optimizationFeatures.forEach(feature => {
|
||||
if (feature.trace_id && feature.organization) {
|
||||
traceIdToOrganization.set(feature.trace_id, feature.organization)
|
||||
}
|
||||
})
|
||||
|
||||
// Trace IDs that have a chosen best candidate (ranking.ranking[0] present)
|
||||
const traceIdsWithBest = new Set(
|
||||
optimizationFeatures
|
||||
.filter(
|
||||
f =>
|
||||
f.trace_id &&
|
||||
(f.ranking as { ranking?: string[] } | null)?.ranking?.[0],
|
||||
)
|
||||
.map(f => f.trace_id.substring(0, 36)),
|
||||
)
|
||||
|
||||
// Get unique call types and models for filters
|
||||
const [callTypes, models] = await Promise.all([
|
||||
prisma.llm_calls.findMany({
|
||||
select: { call_type: true },
|
||||
distinct: ["call_type"],
|
||||
}),
|
||||
prisma.llm_calls.findMany({
|
||||
select: { model_name: true },
|
||||
distinct: ["model_name"],
|
||||
}),
|
||||
])
|
||||
|
||||
const totalPages = Math.ceil(totalCount / pageSize)
|
||||
|
||||
return (
|
||||
<div className="container mx-auto px-4 py-8">
|
||||
<div className="mb-8">
|
||||
{/* Title, Search Bar, and Help Button */}
|
||||
<div className="flex items-center justify-between gap-4 mb-2">
|
||||
<div className="flex items-center gap-3">
|
||||
<h1 className="text-3xl font-bold tracking-tight text-gray-900 dark:text-white whitespace-nowrap">
|
||||
LLM Calls
|
||||
</h1>
|
||||
<HelpButton
|
||||
title="Understanding LLM Calls"
|
||||
content={
|
||||
<div className="space-y-4 text-sm text-gray-700 dark:text-gray-300">
|
||||
<div>
|
||||
<h4 className="font-semibold text-gray-900 dark:text-white mb-2">What is an LLM call?</h4>
|
||||
<p>Each individual API request to an AI model (like GPT-4, Claude, etc.) for generating code optimizations, validations, or other tasks.</p>
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="font-semibold text-gray-900 dark:text-white mb-2">Call sequence (#)</h4>
|
||||
<p>Shows the order of calls within a trace. Multiple calls may be part of the same optimization request.</p>
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="font-semibold text-gray-900 dark:text-white mb-2">Call types</h4>
|
||||
<ul className="list-disc list-inside space-y-1 ml-2">
|
||||
<li><strong>optimization:</strong> Generates optimized code</li>
|
||||
<li><strong>validation:</strong> Checks quality of generated code</li>
|
||||
<li><strong>line_profiler:</strong> Analyzes performance bottlenecks</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="font-semibold text-gray-900 dark:text-white mb-2">Parsing status</h4>
|
||||
<p>Indicates whether the model's response was successfully parsed and extracted into usable code candidates.</p>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
{/* Compact Search Bar */}
|
||||
<form method="get" className="flex items-center gap-2 flex-1 max-w-xl">
|
||||
<input
|
||||
type="text"
|
||||
name="trace_id"
|
||||
placeholder="Search by Trace ID..."
|
||||
defaultValue={searchParams.trace_id || ""}
|
||||
className="flex-1 px-3 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 placeholder-gray-500 dark:placeholder-gray-400 focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-shadow"
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
className="px-4 py-2 text-sm bg-blue-600 dark:bg-blue-700 text-white rounded hover:bg-blue-700 dark:hover:bg-blue-600 whitespace-nowrap"
|
||||
>
|
||||
Search
|
||||
</button>
|
||||
{searchParams.trace_id && (
|
||||
<Link
|
||||
href="/observability/llm-calls"
|
||||
className="px-4 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded hover:bg-gray-50 dark:hover:bg-gray-700 text-gray-900 dark:text-gray-100 whitespace-nowrap"
|
||||
>
|
||||
Clear
|
||||
</Link>
|
||||
)}
|
||||
</form>
|
||||
</div>
|
||||
<p className="text-gray-600 dark:text-gray-400">
|
||||
Track and analyze all LLM API calls for prompt engineering
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Filters */}
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg shadow p-5 mb-8 border border-gray-200 dark:border-gray-700 hover:shadow-md transition-shadow">
|
||||
<form method="get" className="flex flex-wrap gap-4">
|
||||
<div>
|
||||
<label className="flex items-center gap-1.5 text-sm font-semibold mb-1 text-gray-700 dark:text-gray-300">
|
||||
Call Type
|
||||
<InfoIcon content="Type of operation: optimization generates code, validation checks quality, etc." side="top" />
|
||||
</label>
|
||||
<select
|
||||
name="call_type"
|
||||
className="px-3 py-2 border border-gray-300 dark:border-gray-600 rounded bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||||
defaultValue={searchParams.call_type || ""}
|
||||
>
|
||||
<option value="">All</option>
|
||||
{callTypes.map(ct => (
|
||||
<option key={ct.call_type} value={ct.call_type}>
|
||||
{ct.call_type}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="flex items-center gap-1.5 text-sm font-semibold mb-1 text-gray-700 dark:text-gray-300">
|
||||
Model
|
||||
<InfoIcon content="AI model used (e.g., gpt-4, claude-3, etc.)" side="top" />
|
||||
</label>
|
||||
<select
|
||||
name="model"
|
||||
className="px-3 py-2 border border-gray-300 dark:border-gray-600 rounded bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||||
defaultValue={searchParams.model || ""}
|
||||
>
|
||||
<option value="">All</option>
|
||||
{models.map(m => (
|
||||
<option key={m.model_name} value={m.model_name}>
|
||||
{m.model_name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="flex items-center gap-1.5 text-sm font-semibold mb-1 text-gray-700 dark:text-gray-300">
|
||||
Status
|
||||
<InfoIcon content="success = completed normally, failed = error occurred, partial = incomplete results" side="top" />
|
||||
</label>
|
||||
<select
|
||||
name="status"
|
||||
className="px-3 py-2 border border-gray-300 dark:border-gray-600 rounded bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||||
defaultValue={searchParams.status || ""}
|
||||
>
|
||||
<option value="">All</option>
|
||||
<option value="success">Success</option>
|
||||
<option value="partial_success">Partial</option>
|
||||
<option value="failed">Failed</option>
|
||||
<option value="in_progress">In Progress</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="flex items-center gap-1.5 text-sm font-semibold mb-1 text-gray-700 dark:text-gray-300">
|
||||
Organization
|
||||
<InfoIcon content="Organization that made the request" side="top" />
|
||||
</label>
|
||||
<select
|
||||
name="organization"
|
||||
className="px-3 py-2 border border-gray-300 dark:border-gray-600 rounded bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||||
defaultValue={searchParams.organization || ""}
|
||||
>
|
||||
<option value="">All Organizations</option>
|
||||
{uniqueOrganizations.map(org => (
|
||||
<option key={org} value={org}>
|
||||
{org}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="flex items-end gap-2">
|
||||
<button
|
||||
type="submit"
|
||||
className="px-4 py-2 bg-blue-600 dark:bg-blue-700 text-white rounded hover:bg-blue-700 dark:hover:bg-blue-600 focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
|
||||
>
|
||||
Filter
|
||||
</button>
|
||||
{(searchParams.call_type || searchParams.model || searchParams.status || searchParams.organization) && (
|
||||
<Link
|
||||
href="/observability/llm-calls"
|
||||
className="px-4 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded hover:bg-gray-50 dark:hover:bg-gray-700 text-gray-900 dark:text-gray-100"
|
||||
>
|
||||
Clear All
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{/* Stats Summary */}
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-5 mb-8">
|
||||
<StatCard
|
||||
label="Total Calls"
|
||||
value={totalCount}
|
||||
helpText="Total number of LLM API calls matching your filters (across all pages)"
|
||||
icon="Activity"
|
||||
/>
|
||||
<StatCard
|
||||
label="Success Rate"
|
||||
value={`${totalCount > 0 ? Math.round((successCount / totalCount) * 100) : 0}%`}
|
||||
helpText="Percentage of successful calls vs total calls. Excludes in-progress calls."
|
||||
icon="CheckCircle2"
|
||||
variant="success"
|
||||
/>
|
||||
<StatCard
|
||||
label="Total Cost"
|
||||
value={`$${(aggregateStats._sum?.llm_cost ?? 0).toFixed(3)}`}
|
||||
helpText="Cumulative cost of all calls matching filters. Based on model pricing."
|
||||
icon="DollarSign"
|
||||
/>
|
||||
<StatCard
|
||||
label="Avg Latency"
|
||||
value={`${aggregateStats._avg?.latency_ms ? Math.round(aggregateStats._avg.latency_ms) : 0}ms`}
|
||||
helpText="Average time taken for LLM API to respond. Excludes network latency."
|
||||
icon="Clock"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* LLM Calls Table */}
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg shadow overflow-hidden border border-gray-200 dark:border-gray-700">
|
||||
<div className="overflow-x-auto">
|
||||
<table className="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
|
||||
<thead className="bg-gray-50 dark:bg-gray-900">
|
||||
<tr>
|
||||
<ColumnHeader
|
||||
label="#"
|
||||
tooltip="Call sequence number within the trace. Shows execution order."
|
||||
className="px-3 py-3"
|
||||
/>
|
||||
<ColumnHeader
|
||||
label="Timestamp"
|
||||
tooltip="When this API call was made. Click to see full details."
|
||||
/>
|
||||
<ColumnHeader
|
||||
label="Trace ID"
|
||||
tooltip="Parent trace containing this call. Click to view all calls in trace."
|
||||
/>
|
||||
<ColumnHeader
|
||||
label="Organization"
|
||||
tooltip="Organization from trace metadata"
|
||||
/>
|
||||
<ColumnHeader
|
||||
label="Type"
|
||||
tooltip="Call type: what this LLM operation was attempting"
|
||||
/>
|
||||
<ColumnHeader
|
||||
label="Source"
|
||||
tooltip="Origin of request: GitHub Action, CLI, VSCode extension, etc."
|
||||
/>
|
||||
<ColumnHeader
|
||||
label="Model"
|
||||
tooltip="AI model used for this call"
|
||||
/>
|
||||
<ColumnHeader
|
||||
label="Status"
|
||||
tooltip="Call outcome: success, failed, partial_success, or in_progress"
|
||||
/>
|
||||
<ColumnHeader
|
||||
label="Tokens"
|
||||
tooltip="Total tokens (prompt + completion) used in this call"
|
||||
/>
|
||||
<ColumnHeader
|
||||
label="Cost"
|
||||
tooltip="Cost in USD for this specific call"
|
||||
/>
|
||||
<ColumnHeader
|
||||
label="Latency"
|
||||
tooltip="API response time in milliseconds"
|
||||
/>
|
||||
<ColumnHeader
|
||||
label="Candidates"
|
||||
tooltip="Valid candidates / Total generated. For optimization calls only."
|
||||
/>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="bg-white dark:bg-gray-800 divide-y divide-gray-200 dark:divide-gray-700">
|
||||
{llmCalls.map(call => {
|
||||
const ctx = call.context as { call_sequence?: number } | null
|
||||
const callSequence = ctx?.call_sequence
|
||||
const traceIdPrefix = call.trace_id?.substring(0, 36) || ""
|
||||
const eventType = traceIdPrefix ? traceIdToEventType.get(traceIdPrefix) || null : null
|
||||
const organization = traceIdPrefix ? traceIdToOrganization.get(traceIdPrefix) : null
|
||||
const source = getCallSource(eventType, call.context as Record<string, unknown> | null)
|
||||
const isBestOptimizationCall =
|
||||
call.call_type === "optimization" && traceIdsWithBest.has(traceIdPrefix)
|
||||
return (
|
||||
<tr key={call.id} className="hover:bg-gray-50 dark:hover:bg-gray-700/50 hover:shadow-md transition-all duration-200">
|
||||
<td className="px-3 py-5 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400 font-mono">
|
||||
{callSequence ? `#${callSequence}` : "-"}
|
||||
</td>
|
||||
<td className="px-6 py-5 whitespace-nowrap text-sm">
|
||||
<Link
|
||||
href={`/observability/llm-call/${call.id}`}
|
||||
className="text-blue-600 dark:text-blue-400 hover:underline"
|
||||
>
|
||||
{new Date(call.created_at).toLocaleString()}
|
||||
</Link>
|
||||
</td>
|
||||
<td className="px-6 py-5 whitespace-nowrap text-sm">
|
||||
{call.trace_id && call.trace_id.trim() ? (
|
||||
<Link
|
||||
href={`/observability/trace/${call.trace_id}`}
|
||||
className="text-purple-600 dark:text-purple-400 hover:underline font-mono text-xs"
|
||||
>
|
||||
{call.trace_id.substring(0, 8)}...
|
||||
</Link>
|
||||
) : (
|
||||
<span className="text-gray-400 dark:text-gray-500 font-mono text-xs">N/A</span>
|
||||
)}
|
||||
</td>
|
||||
<td className="px-6 py-5 whitespace-nowrap text-sm text-gray-700 dark:text-gray-300">
|
||||
{organization || "N/A"}
|
||||
</td>
|
||||
<td className="px-6 py-5 whitespace-nowrap text-sm">
|
||||
<span className="inline-flex items-center gap-1.5">
|
||||
<span className="px-2 py-1 bg-blue-100 dark:bg-blue-900 text-blue-800 dark:text-blue-200 rounded">
|
||||
{call.call_type}
|
||||
</span>
|
||||
{isBestOptimizationCall && (
|
||||
<span
|
||||
className="inline-flex items-center gap-1 px-2 py-0.5 bg-amber-100 dark:bg-amber-900 text-amber-800 dark:text-amber-200 rounded text-xs font-semibold"
|
||||
title="Optimization that produced the chosen candidate for this trace"
|
||||
>
|
||||
<Award className="h-3.5 w-3.5" />
|
||||
Best
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-6 py-5 whitespace-nowrap text-sm">
|
||||
<span className="inline-flex items-center gap-1.5 px-2 py-1 bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300 rounded text-xs">
|
||||
{source.toLowerCase().includes("github") ? (
|
||||
<Github className="h-3 w-3" />
|
||||
) : source.toLowerCase().includes("vscode") || source.toLowerCase().includes("cli") ? (
|
||||
<Terminal className="h-3 w-3" />
|
||||
) : null}
|
||||
{source}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-6 py-5 whitespace-nowrap text-sm text-gray-900 dark:text-gray-100">
|
||||
{call.model_name}
|
||||
</td>
|
||||
<td className="px-6 py-5 whitespace-nowrap text-sm">
|
||||
<span
|
||||
className={`px-2 py-1 rounded ${
|
||||
call.status === "success"
|
||||
? "bg-green-100 dark:bg-green-900 text-green-800 dark:text-green-200"
|
||||
: call.status === "failed"
|
||||
? "bg-red-100 dark:bg-red-900 text-red-800 dark:text-red-200"
|
||||
: "bg-yellow-100 dark:bg-yellow-900 text-yellow-800 dark:text-yellow-200"
|
||||
}`}
|
||||
>
|
||||
{call.status}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-6 py-5 whitespace-nowrap text-sm text-gray-900 dark:text-gray-100">
|
||||
{call.prompt_tokens && call.completion_tokens
|
||||
? `${call.prompt_tokens + call.completion_tokens}`
|
||||
: "-"}
|
||||
</td>
|
||||
<td className="px-6 py-5 whitespace-nowrap text-sm text-gray-900 dark:text-gray-100">
|
||||
{call.llm_cost ? `$${call.llm_cost.toFixed(4)}` : "-"}
|
||||
</td>
|
||||
<td className="px-6 py-5 whitespace-nowrap text-sm text-gray-900 dark:text-gray-100">
|
||||
{call.latency_ms ? `${call.latency_ms}ms` : "-"}
|
||||
</td>
|
||||
<td className="px-6 py-5 whitespace-nowrap text-sm text-gray-900 dark:text-gray-100">
|
||||
{call.candidates_valid}/{call.candidates_generated || 0}
|
||||
</td>
|
||||
</tr>
|
||||
)
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{llmCalls.length === 0 && (
|
||||
<div className="flex flex-col items-center justify-center py-16 text-center">
|
||||
<DatabaseIcon className="h-12 w-12 text-gray-400 dark:text-gray-500 mb-4" />
|
||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-2">
|
||||
No LLM Calls Found
|
||||
</h3>
|
||||
{searchParams.call_type || searchParams.model || searchParams.status || searchParams.organization || searchParams.trace_id ? (
|
||||
<div className="space-y-3">
|
||||
<p className="text-gray-500 dark:text-gray-400">
|
||||
Try adjusting your filters above
|
||||
</p>
|
||||
<Link
|
||||
href="/observability/llm-calls"
|
||||
className="inline-flex items-center px-4 py-2 text-sm font-medium text-blue-600 dark:text-blue-400 hover:text-blue-700 dark:hover:text-blue-300 border border-blue-200 dark:border-blue-800 rounded-md hover:bg-blue-50 dark:hover:bg-blue-900/20 transition-colors"
|
||||
>
|
||||
Clear All Filters
|
||||
</Link>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
<p className="text-gray-500 dark:text-gray-400">
|
||||
Run the test script to generate sample data
|
||||
</p>
|
||||
<code className="block bg-gray-100 dark:bg-gray-700 text-gray-800 dark:text-gray-200 px-4 py-2 rounded-md text-sm font-mono">
|
||||
python django/aiservice/test_observability_local.py
|
||||
</code>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Pagination */}
|
||||
{totalPages > 1 && (
|
||||
<div className="mt-6 flex justify-center gap-2">
|
||||
{page > 1 && (
|
||||
<Link
|
||||
href={`?page=${page - 1}${searchParams.call_type ? `&call_type=${searchParams.call_type}` : ""}${searchParams.model ? `&model=${searchParams.model}` : ""}${searchParams.status ? `&status=${searchParams.status}` : ""}${searchParams.organization ? `&organization=${searchParams.organization}` : ""}`}
|
||||
className="px-4 py-2 border border-gray-300 dark:border-gray-600 rounded hover:bg-gray-50 dark:hover:bg-gray-700 text-gray-900 dark:text-gray-100"
|
||||
>
|
||||
Previous
|
||||
</Link>
|
||||
)}
|
||||
<span className="px-4 py-2 text-gray-900 dark:text-gray-100">
|
||||
Page {page} of {totalPages}
|
||||
</span>
|
||||
{page < totalPages && (
|
||||
<Link
|
||||
href={`?page=${page + 1}${searchParams.call_type ? `&call_type=${searchParams.call_type}` : ""}${searchParams.model ? `&model=${searchParams.model}` : ""}${searchParams.status ? `&status=${searchParams.status}` : ""}${searchParams.organization ? `&organization=${searchParams.organization}` : ""}`}
|
||||
className="px-4 py-2 border border-gray-300 dark:border-gray-600 rounded hover:bg-gray-50 dark:hover:bg-gray-700 text-gray-900 dark:text-gray-100"
|
||||
>
|
||||
Next
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
} catch (error) {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
|
@ -1,39 +0,0 @@
|
|||
export default function ObservabilityLoading() {
|
||||
return (
|
||||
<div className="min-h-screen bg-zinc-50 dark:bg-zinc-900">
|
||||
{/* Search Section Skeleton */}
|
||||
<div className="border-b border-zinc-200 dark:border-zinc-700 bg-white dark:bg-zinc-800">
|
||||
<div className="max-w-4xl mx-auto px-4 py-4">
|
||||
<div className="w-full max-w-2xl mx-auto">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex-1 h-12 bg-zinc-200 dark:bg-zinc-700 rounded-md animate-pulse" />
|
||||
<div className="w-24 h-12 bg-zinc-200 dark:bg-zinc-700 rounded-md animate-pulse" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Content Skeleton */}
|
||||
<div className="max-w-4xl mx-auto px-4 py-8">
|
||||
{/* Progress skeleton */}
|
||||
<div className="mb-8">
|
||||
<div className="h-4 w-48 bg-zinc-200 dark:bg-zinc-700 rounded mb-2 animate-pulse" />
|
||||
<div className="h-1 w-full bg-zinc-200 dark:bg-zinc-700 rounded animate-pulse" />
|
||||
</div>
|
||||
|
||||
{/* Timeline items skeleton */}
|
||||
<div className="space-y-8">
|
||||
{[1, 2, 3].map((i) => (
|
||||
<div key={i} className="flex gap-4">
|
||||
<div className="w-5 h-5 rounded-full bg-zinc-200 dark:bg-zinc-700 animate-pulse" />
|
||||
<div className="flex-1">
|
||||
<div className="h-6 w-32 bg-zinc-200 dark:bg-zinc-700 rounded mb-3 animate-pulse" />
|
||||
<div className="h-48 bg-zinc-200 dark:bg-zinc-700 rounded animate-pulse" />
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -1,328 +0,0 @@
|
|||
import { Suspense } from "react"
|
||||
import { unstable_cache } from "next/cache"
|
||||
import { Search } from "lucide-react"
|
||||
import { prisma } from "@/lib/prisma"
|
||||
import { TraceSearch } from "@/components/observability/trace-search"
|
||||
import { TimelinePageView } from "@/components/observability/timeline-page-view"
|
||||
import { transformToTimelineSections } from "@/components/observability/timeline-types"
|
||||
import { ErrorsSection } from "@/components/observability/errors-section"
|
||||
import { FunctionToOptimizeSection } from "@/components/observability/function-to-optimize-section"
|
||||
import { CodeContextSection } from "@/components/observability/code-context-section"
|
||||
|
||||
export const revalidate = 60
|
||||
|
||||
interface Observability2PageProps {
|
||||
searchParams: Promise<{
|
||||
trace_id?: string
|
||||
}>
|
||||
}
|
||||
|
||||
const getTraceData = unstable_cache(
|
||||
async (tracePrefix: string) => {
|
||||
const [rawLlmCalls, errors, optimizationFeatures, optimizationEvent] = await Promise.all([
|
||||
prisma.llm_calls.findMany({
|
||||
where: { trace_id: { startsWith: tracePrefix } },
|
||||
orderBy: { created_at: "asc" },
|
||||
}),
|
||||
prisma.optimization_errors.findMany({
|
||||
where: { trace_id: { startsWith: tracePrefix } },
|
||||
orderBy: { created_at: "asc" },
|
||||
}),
|
||||
prisma.optimization_features.findFirst({
|
||||
where: { trace_id: { startsWith: tracePrefix } },
|
||||
}),
|
||||
prisma.optimization_events.findFirst({
|
||||
where: { trace_id: { startsWith: tracePrefix } },
|
||||
select: { event_type: true, function_name: true, file_path: true },
|
||||
}),
|
||||
])
|
||||
return { rawLlmCalls, errors, optimizationFeatures, optimizationEvent }
|
||||
},
|
||||
["observability-trace-detail"],
|
||||
{ revalidate: 60 },
|
||||
)
|
||||
|
||||
export default async function Observability2Page({ searchParams }: Observability2PageProps) {
|
||||
const params = await searchParams
|
||||
const traceId = params.trace_id?.trim()
|
||||
|
||||
let traceData: Awaited<ReturnType<typeof getTraceData>> | null = null
|
||||
if (traceId) {
|
||||
const tracePrefix = traceId.substring(0, 33)
|
||||
traceData = await getTraceData(tracePrefix)
|
||||
}
|
||||
|
||||
const hasResults = traceData
|
||||
? traceData.rawLlmCalls.length > 0 || traceData.errors.length > 0
|
||||
: false
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-zinc-50 dark:bg-zinc-900">
|
||||
<div className="border-b border-zinc-200 dark:border-zinc-700 bg-white dark:bg-zinc-800">
|
||||
<div className="max-w-4xl mx-auto px-4 py-4">
|
||||
<Suspense fallback={<SearchSkeleton />}>
|
||||
<TraceSearch initialTraceId={traceId || ""} hasResults={hasResults} />
|
||||
</Suspense>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{traceId && traceData ? (
|
||||
<Suspense fallback={<TraceContentSkeleton />}>
|
||||
<TraceContent traceId={traceId} traceData={traceData} />
|
||||
</Suspense>
|
||||
) : traceId ? (
|
||||
<TraceContentSkeleton />
|
||||
) : (
|
||||
<EmptyState />
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
interface TraceData {
|
||||
rawLlmCalls: Awaited<ReturnType<typeof getTraceData>>["rawLlmCalls"]
|
||||
errors: Awaited<ReturnType<typeof getTraceData>>["errors"]
|
||||
optimizationFeatures: Awaited<ReturnType<typeof getTraceData>>["optimizationFeatures"]
|
||||
optimizationEvent: Awaited<ReturnType<typeof getTraceData>>["optimizationEvent"]
|
||||
}
|
||||
|
||||
function TraceContent({ traceId, traceData }: { traceId: string; traceData: TraceData }) {
|
||||
const { rawLlmCalls, errors, optimizationFeatures, optimizationEvent } = traceData
|
||||
|
||||
if (rawLlmCalls.length === 0 && errors.length === 0) {
|
||||
return <NotFoundState traceId={traceId} />
|
||||
}
|
||||
|
||||
const optimizationsOrigin =
|
||||
(optimizationFeatures?.optimizations_origin as Record<
|
||||
string,
|
||||
{ source: string; model?: string; call_sequence?: number; parent?: string }
|
||||
>) || {}
|
||||
|
||||
const candidateExplanations =
|
||||
(optimizationFeatures?.explanations_post as Record<string, string>) || {}
|
||||
|
||||
const allCandidates = optimizationFeatures?.optimizations_post
|
||||
? Object.entries(optimizationFeatures.optimizations_post as Record<string, string>).map(
|
||||
([id, code]) => ({
|
||||
id,
|
||||
code: typeof code === "string" ? code : "",
|
||||
source: optimizationsOrigin[id]?.source || "OPTIMIZE",
|
||||
model: optimizationsOrigin[id]?.model,
|
||||
callSequence: optimizationsOrigin[id]?.call_sequence,
|
||||
explanation: candidateExplanations[id],
|
||||
}),
|
||||
)
|
||||
: []
|
||||
|
||||
const optimizationCandidates = allCandidates
|
||||
.filter(c => c.source === "OPTIMIZE")
|
||||
.sort((a, b) => (a.callSequence ?? Infinity) - (b.callSequence ?? Infinity))
|
||||
.map((c, index) => ({ ...c, index: index + 1 }))
|
||||
|
||||
const lineProfilerCandidates = allCandidates
|
||||
.filter(c => c.source === "OPTIMIZE_LP")
|
||||
.sort((a, b) => (a.callSequence ?? Infinity) - (b.callSequence ?? Infinity))
|
||||
.map((c, index) => ({ ...c, index: index + 1 }))
|
||||
|
||||
const refinementCandidates = allCandidates
|
||||
.filter(c => c.source === "REFINE")
|
||||
.sort((a, b) => (a.callSequence ?? Infinity) - (b.callSequence ?? Infinity))
|
||||
.map((c, index) => ({
|
||||
...c,
|
||||
index: index + 1,
|
||||
parentId: optimizationsOrigin[c.id]?.parent || null,
|
||||
}))
|
||||
|
||||
const rankingData = optimizationFeatures?.ranking as
|
||||
| { ranking?: string[]; explanation?: string }
|
||||
| null
|
||||
const bestCandidateId = rankingData?.ranking?.[0] ?? null
|
||||
|
||||
const pullRequestRaw = optimizationFeatures?.pull_request
|
||||
const usedForPr = Boolean(
|
||||
pullRequestRaw != null &&
|
||||
typeof pullRequestRaw === "object" &&
|
||||
!Array.isArray(pullRequestRaw) &&
|
||||
Object.keys(pullRequestRaw as Record<string, unknown>).length > 0,
|
||||
)
|
||||
|
||||
const candidateRankMap: Record<string, number> = {}
|
||||
if (rankingData?.ranking) {
|
||||
rankingData.ranking.forEach((id, index) => {
|
||||
candidateRankMap[id] = index + 1
|
||||
})
|
||||
}
|
||||
|
||||
const generatedTests = (optimizationFeatures?.generated_test ?? []).map((code, index) => ({
|
||||
code,
|
||||
index: index + 1,
|
||||
}))
|
||||
|
||||
const instrumentedTests = (optimizationFeatures?.instrumented_generated_test ?? []).map((code, index) => ({
|
||||
code,
|
||||
index: index + 1,
|
||||
}))
|
||||
|
||||
const instrumentedPerfTests = ((optimizationFeatures as Record<string, unknown>)?.instrumented_perf_test as string[] ?? []).map((code, index) => ({
|
||||
code,
|
||||
index: index + 1,
|
||||
}))
|
||||
|
||||
const llmCalls = rawLlmCalls.sort((a, b) => {
|
||||
const seqA = (a.context as { call_sequence?: number } | null)?.call_sequence ?? Infinity
|
||||
const seqB = (b.context as { call_sequence?: number } | null)?.call_sequence ?? Infinity
|
||||
if (seqA !== seqB) return seqA - seqB
|
||||
return new Date(a.created_at).getTime() - new Date(b.created_at).getTime()
|
||||
})
|
||||
|
||||
const transformedCalls = llmCalls.map(call => ({
|
||||
id: call.id,
|
||||
call_type: call.call_type,
|
||||
model_name: call.model_name,
|
||||
status: call.status,
|
||||
latency_ms: call.latency_ms,
|
||||
llm_cost: call.llm_cost,
|
||||
total_tokens: call.total_tokens,
|
||||
created_at: call.created_at,
|
||||
context: call.context as { call_sequence?: number } | null,
|
||||
}))
|
||||
|
||||
const { sections, totalDuration } = transformToTimelineSections({
|
||||
calls: transformedCalls,
|
||||
optimizationCandidates,
|
||||
lineProfilerCandidates,
|
||||
refinementCandidates,
|
||||
generatedTests,
|
||||
instrumentedTests,
|
||||
instrumentedPerfTests,
|
||||
originalCode: optimizationFeatures?.original_code ?? null,
|
||||
testFramework: optimizationFeatures?.test_framework ?? null,
|
||||
candidateRankMap,
|
||||
bestCandidateId,
|
||||
rankingExplanation: rankingData?.explanation ?? null,
|
||||
usedForPr,
|
||||
})
|
||||
|
||||
const transformedErrors = errors.map(error => ({
|
||||
id: error.id,
|
||||
error_type: error.error_type,
|
||||
severity: error.severity,
|
||||
error_message: error.error_message,
|
||||
context: error.context as {
|
||||
test_name?: string
|
||||
failure_reason?: string
|
||||
test_output?: string
|
||||
expected?: string
|
||||
actual?: string
|
||||
} | null,
|
||||
created_at: error.created_at,
|
||||
}))
|
||||
|
||||
const functionName = (optimizationFeatures?.metadata as Record<string, unknown>)?.function_to_optimize as string ?? optimizationEvent?.function_name ?? null
|
||||
const filePath = optimizationEvent?.file_path ?? null
|
||||
const originalCode = optimizationFeatures?.original_code ?? null
|
||||
const dependencyCode = optimizationFeatures?.dependency_code ?? null
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="max-w-4xl mx-auto px-4 py-6 space-y-4">
|
||||
<FunctionToOptimizeSection
|
||||
functionName={functionName}
|
||||
filePath={filePath}
|
||||
originalCode={originalCode}
|
||||
/>
|
||||
<CodeContextSection
|
||||
functionName={functionName}
|
||||
filePath={filePath}
|
||||
originalCode={originalCode}
|
||||
dependencyCode={dependencyCode}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<TimelinePageView
|
||||
sections={sections}
|
||||
totalDuration={totalDuration}
|
||||
functionName={functionName}
|
||||
filePath={filePath}
|
||||
/>
|
||||
|
||||
{transformedErrors.length > 0 && (
|
||||
<div className="max-w-4xl mx-auto px-4 pb-20">
|
||||
<ErrorsSection errors={transformedErrors} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function EmptyState() {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center py-32 text-center px-4">
|
||||
<div className="w-20 h-20 mb-8 rounded-full bg-zinc-100 dark:bg-zinc-800 flex items-center justify-center">
|
||||
<Search className="h-10 w-10 text-zinc-400" />
|
||||
</div>
|
||||
<h3 className="text-2xl font-semibold text-zinc-900 dark:text-zinc-50 mb-3">
|
||||
Enter a Trace ID to Get Started
|
||||
</h3>
|
||||
<p className="text-zinc-500 dark:text-zinc-400 max-w-md leading-relaxed">
|
||||
Paste or type a trace ID in the search box above to view the complete optimization timeline,
|
||||
including all LLM calls, generated candidates, and any errors.
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function NotFoundState({ traceId }: { traceId: string }) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center py-32 text-center px-4">
|
||||
<div className="w-20 h-20 mb-8 rounded-full bg-red-100 dark:bg-red-900/30 flex items-center justify-center">
|
||||
<Search className="h-10 w-10 text-red-500" />
|
||||
</div>
|
||||
<h3 className="text-2xl font-semibold text-zinc-900 dark:text-zinc-50 mb-3">Trace Not Found</h3>
|
||||
<p className="text-zinc-500 dark:text-zinc-400 max-w-md mb-6">
|
||||
No data was found for the trace ID:
|
||||
</p>
|
||||
<code className="text-sm font-mono text-zinc-900 dark:text-zinc-50 bg-zinc-100 dark:bg-zinc-700 px-4 py-2 rounded-md">
|
||||
{traceId}
|
||||
</code>
|
||||
<p className="text-zinc-500 dark:text-zinc-400 max-w-md mt-6 text-sm">
|
||||
Please check that the trace ID is correct and try again.
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function SearchSkeleton() {
|
||||
return (
|
||||
<div className="w-full max-w-2xl mx-auto">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex-1 h-12 bg-zinc-200 dark:bg-zinc-700 rounded-md animate-pulse" />
|
||||
<div className="w-24 h-12 bg-zinc-200 dark:bg-zinc-700 rounded-md animate-pulse" />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function TraceContentSkeleton() {
|
||||
return (
|
||||
<div className="max-w-4xl mx-auto px-4 py-8">
|
||||
<div className="mb-8">
|
||||
<div className="h-4 w-48 bg-zinc-200 dark:bg-zinc-700 rounded mb-2 animate-pulse" />
|
||||
<div className="h-1 w-full bg-zinc-200 dark:bg-zinc-700 rounded animate-pulse" />
|
||||
</div>
|
||||
|
||||
<div className="space-y-8">
|
||||
{[1, 2, 3].map((i) => (
|
||||
<div key={i} className="flex gap-4">
|
||||
<div className="w-5 h-5 rounded-full bg-zinc-200 dark:bg-zinc-700 animate-pulse" />
|
||||
<div className="flex-1">
|
||||
<div className="h-6 w-32 bg-zinc-200 dark:bg-zinc-700 rounded mb-3 animate-pulse" />
|
||||
<div className="h-48 bg-zinc-200 dark:bg-zinc-700 rounded animate-pulse" />
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
767
js/cf-webapp/src/app/observability/trace/[trace_id]/page.tsx
Normal file
767
js/cf-webapp/src/app/observability/trace/[trace_id]/page.tsx
Normal file
|
|
@ -0,0 +1,767 @@
|
|||
import Link from "next/link"
|
||||
import { notFound } from "next/navigation"
|
||||
import { unstable_cache } from "next/cache"
|
||||
import {
|
||||
CheckCircle,
|
||||
Timer,
|
||||
DollarSign,
|
||||
Hash,
|
||||
Code as CodeIcon,
|
||||
Github,
|
||||
Terminal,
|
||||
AlertCircle,
|
||||
XCircle,
|
||||
} from "lucide-react"
|
||||
import { prisma } from "@/lib/prisma"
|
||||
import { getCallSource } from "@/lib/observability-utils"
|
||||
import { HelpButton } from "@/components/observability/help-button"
|
||||
import { InfoIcon } from "@/components/observability/info-icon"
|
||||
import { CopyButton } from "@/components/observability/copy-button"
|
||||
|
||||
export const revalidate = 60
|
||||
|
||||
interface TracePageProps {
|
||||
params: {
|
||||
trace_id: string
|
||||
}
|
||||
}
|
||||
|
||||
const getTraceData = unstable_cache(
|
||||
async (tracePrefix: string) => {
|
||||
const [rawLlmCalls, errors, optimizationFeatures, optimizationEvent] = await Promise.all([
|
||||
prisma.llm_calls.findMany({
|
||||
where: { trace_id: { startsWith: tracePrefix } },
|
||||
orderBy: { created_at: "asc" },
|
||||
}),
|
||||
prisma.optimization_errors.findMany({
|
||||
where: { trace_id: { startsWith: tracePrefix } },
|
||||
orderBy: { created_at: "asc" },
|
||||
}),
|
||||
prisma.optimization_features.findFirst({
|
||||
where: { trace_id: { startsWith: tracePrefix } },
|
||||
}),
|
||||
prisma.optimization_events.findFirst({
|
||||
where: { trace_id: { startsWith: tracePrefix } },
|
||||
select: { event_type: true },
|
||||
}),
|
||||
])
|
||||
return { rawLlmCalls, errors, optimizationFeatures, optimizationEvent }
|
||||
},
|
||||
["trace-detail"],
|
||||
{ revalidate: 60 },
|
||||
)
|
||||
|
||||
export default async function TracePage({ params }: TracePageProps) {
|
||||
const { trace_id } = params
|
||||
|
||||
// Use prefix matching (first 33 chars) to group multi-model calls that share the same base trace_id
|
||||
const tracePrefix = trace_id.substring(0, 33)
|
||||
const { rawLlmCalls, errors, optimizationFeatures, optimizationEvent } = await getTraceData(tracePrefix)
|
||||
|
||||
const traceSource = getCallSource(optimizationEvent?.event_type || null, null)
|
||||
|
||||
// Extract optimization candidates from optimization_features
|
||||
// Also get the origin of each candidate (OPTIMIZE vs OPTIMIZE_LP) and model info
|
||||
const optimizationsOrigin =
|
||||
(optimizationFeatures?.optimizations_origin as Record<
|
||||
string,
|
||||
{ source: string; model?: string }
|
||||
>) || {}
|
||||
|
||||
const allCandidates = optimizationFeatures?.optimizations_post
|
||||
? Object.entries(optimizationFeatures.optimizations_post as Record<string, string>).map(
|
||||
([id, code]) => ({
|
||||
id,
|
||||
code: typeof code === "string" ? code : "",
|
||||
source: optimizationsOrigin[id]?.source || "OPTIMIZE",
|
||||
model: optimizationsOrigin[id]?.model,
|
||||
}),
|
||||
)
|
||||
: []
|
||||
|
||||
// Filter candidates by source for display under the correct section
|
||||
const optimizationCandidates = allCandidates
|
||||
.filter(c => c.source === "OPTIMIZE")
|
||||
.map((c, index) => ({ ...c, index: index + 1 }))
|
||||
|
||||
const lineProfilerCandidates = allCandidates
|
||||
.filter(c => c.source === "OPTIMIZE_LP")
|
||||
.map((c, index) => ({ ...c, index: index + 1 }))
|
||||
|
||||
// Get explanations for candidates if available
|
||||
const candidateExplanations =
|
||||
(optimizationFeatures?.explanations_post as Record<string, string>) || {}
|
||||
|
||||
// Best candidate (first in ranking) and whether it was used for PR
|
||||
const rankingData = optimizationFeatures?.ranking as
|
||||
| { ranking?: string[]; explanation?: string }
|
||||
| null
|
||||
const bestCandidateId = rankingData?.ranking?.[0] ?? null
|
||||
const pullRequestRaw = optimizationFeatures?.pull_request
|
||||
const usedForPr = Boolean(
|
||||
pullRequestRaw != null &&
|
||||
typeof pullRequestRaw === "object" &&
|
||||
!Array.isArray(pullRequestRaw) &&
|
||||
Object.keys(pullRequestRaw as Record<string, unknown>).length > 0,
|
||||
)
|
||||
|
||||
// Map candidate ID to rank position (1-based, 1 = best)
|
||||
const candidateRankMap = new Map<string, number>()
|
||||
if (rankingData?.ranking) {
|
||||
rankingData.ranking.forEach((id, index) => {
|
||||
candidateRankMap.set(id, index + 1)
|
||||
})
|
||||
}
|
||||
|
||||
// Sort by call_sequence from context if available, otherwise by created_at
|
||||
const llmCalls = rawLlmCalls.sort((a, b) => {
|
||||
const seqA = (a.context as { call_sequence?: number } | null)?.call_sequence ?? Infinity
|
||||
const seqB = (b.context as { call_sequence?: number } | null)?.call_sequence ?? Infinity
|
||||
if (seqA !== seqB) return seqA - seqB
|
||||
return new Date(a.created_at).getTime() - new Date(b.created_at).getTime()
|
||||
})
|
||||
|
||||
// If no data found, show 404
|
||||
if (llmCalls.length === 0 && errors.length === 0) {
|
||||
notFound()
|
||||
}
|
||||
|
||||
// Calculate summary metrics
|
||||
const totalCost = llmCalls.reduce((sum, call) => sum + (call.llm_cost ?? 0), 0)
|
||||
const totalTokens = llmCalls.reduce((sum, call) => sum + (call.total_tokens ?? 0), 0)
|
||||
const failedCalls = llmCalls.filter(c => c.status === "failed").length
|
||||
|
||||
// Calculate timeline data using Math.min/Math.max to handle out-of-order timestamps
|
||||
const timestamps = llmCalls.map(call => new Date(call.created_at).getTime())
|
||||
const minTimestamp = timestamps.length > 0 ? Math.min(...timestamps) : 0
|
||||
const maxTimestamp = timestamps.length > 0 ? Math.max(...timestamps) : 0
|
||||
const totalDuration = maxTimestamp > minTimestamp ? (maxTimestamp - minTimestamp) / 1000 : 0
|
||||
|
||||
// Status determination - check for partial_success, failed, or success
|
||||
const hasPartial = llmCalls.some(c => c.status === "partial_success")
|
||||
const status = failedCalls > 0 ? "Failed" : hasPartial ? "Partial" : "Completed"
|
||||
const statusColor =
|
||||
failedCalls > 0
|
||||
? "text-red-600 dark:text-red-400"
|
||||
: hasPartial
|
||||
? "text-yellow-600 dark:text-yellow-400"
|
||||
: "text-green-600 dark:text-green-400"
|
||||
|
||||
// Group calls by call_type
|
||||
const groupedCalls = llmCalls.reduce(
|
||||
(acc, call) => {
|
||||
const type = call.call_type || "unknown"
|
||||
if (!acc[type]) {
|
||||
acc[type] = []
|
||||
}
|
||||
acc[type].push(call)
|
||||
return acc
|
||||
},
|
||||
{} as Record<string, typeof llmCalls>,
|
||||
)
|
||||
|
||||
// Get call types in order of first appearance
|
||||
const orderedTypes = [...new Set(llmCalls.map(c => c.call_type || "unknown"))]
|
||||
|
||||
// Create a map of call_type to LLM call for candidate linking
|
||||
const callTypeToLlmCall = new Map<string, typeof llmCalls[0]>()
|
||||
llmCalls.forEach(call => {
|
||||
if (call.call_type && !callTypeToLlmCall.has(call.call_type)) {
|
||||
callTypeToLlmCall.set(call.call_type, call)
|
||||
}
|
||||
})
|
||||
|
||||
return (
|
||||
<div className="container mx-auto p-6">
|
||||
{/* Header */}
|
||||
<div className="mb-8">
|
||||
<div className="flex items-center gap-2 text-sm text-gray-600 dark:text-gray-400 mb-4">
|
||||
<Link
|
||||
href="/observability/traces"
|
||||
className="hover:text-blue-600 dark:hover:text-blue-400"
|
||||
>
|
||||
Traces
|
||||
</Link>
|
||||
<span>/</span>
|
||||
<span className="text-gray-900 dark:text-white font-mono">
|
||||
{trace_id && trace_id.trim() ? `${trace_id.substring(0, 8)}...` : "N/A"}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between gap-4">
|
||||
<div className="flex-1">
|
||||
<h2 className="text-2xl font-bold tracking-tight text-gray-900 dark:text-white mb-2">
|
||||
Trace Details
|
||||
</h2>
|
||||
<div className="flex items-center gap-2">
|
||||
<p className="text-gray-600 dark:text-gray-400 font-mono text-sm">
|
||||
{trace_id && trace_id.trim() ? trace_id : "Invalid Trace ID"}
|
||||
</p>
|
||||
{trace_id && trace_id.trim() && <CopyButton text={trace_id} label="trace ID" />}
|
||||
</div>
|
||||
</div>
|
||||
<HelpButton
|
||||
title="Trace Detail Page Guide"
|
||||
content={
|
||||
<div className="space-y-4 text-sm text-gray-700 dark:text-gray-300">
|
||||
<div>
|
||||
<h4 className="font-semibold text-gray-900 dark:text-white mb-2">
|
||||
Summary Metrics
|
||||
</h4>
|
||||
<p>
|
||||
View key metrics including status, source, duration, cost, tokens, and number
|
||||
of generated candidates for this optimization request.
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="font-semibold text-gray-900 dark:text-white mb-2">
|
||||
LLM Calls Timeline
|
||||
</h4>
|
||||
<p>
|
||||
All LLM API calls grouped by type. Expand each call to see detailed metrics
|
||||
including tokens, latency, and timestamp. For optimization and line_profiler
|
||||
types, candidates are displayed directly.
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="font-semibold text-gray-900 dark:text-white mb-2">
|
||||
Generated Candidates
|
||||
</h4>
|
||||
<p>
|
||||
Code optimization candidates generated during this trace. Each candidate includes
|
||||
an explanation and the generated code. Use the copy button to copy candidate code.
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="font-semibold text-gray-900 dark:text-white mb-2">
|
||||
Error Handling
|
||||
</h4>
|
||||
<p>
|
||||
If any errors occurred during optimization, they are displayed with severity
|
||||
indicators, error messages, and detailed context for test failures.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Summary Card */}
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg shadow p-6 border border-gray-200 dark:border-gray-700 mb-8">
|
||||
<div className="grid grid-cols-2 md:grid-cols-6 gap-6">
|
||||
<div>
|
||||
<div className="flex items-center gap-1.5 text-sm text-gray-600 dark:text-gray-400 font-medium mb-2">
|
||||
{status === "Completed" ? (
|
||||
<CheckCircle className="h-4 w-4" />
|
||||
) : status === "Failed" ? (
|
||||
<XCircle className="h-4 w-4" />
|
||||
) : (
|
||||
<AlertCircle className="h-4 w-4" />
|
||||
)}
|
||||
<span>Status</span>
|
||||
<InfoIcon
|
||||
content="Overall trace status based on all contained calls"
|
||||
side="top"
|
||||
/>
|
||||
</div>
|
||||
<div className={`text-xl font-bold ${statusColor}`}>{status}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="flex items-center gap-1.5 text-sm text-gray-600 dark:text-gray-400 font-medium mb-2">
|
||||
{traceSource.toLowerCase().includes("github") ? (
|
||||
<Github className="h-4 w-4" />
|
||||
) : (
|
||||
<Terminal className="h-4 w-4" />
|
||||
)}
|
||||
<span>Source</span>
|
||||
<InfoIcon
|
||||
content="Where this optimization was triggered from"
|
||||
side="top"
|
||||
/>
|
||||
</div>
|
||||
<div className="text-xl font-bold text-gray-900 dark:text-white">
|
||||
<span className="px-2 py-1 bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300 rounded text-sm">
|
||||
{traceSource}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="flex items-center gap-1.5 text-sm text-gray-600 dark:text-gray-400 font-medium mb-2">
|
||||
<Timer className="h-4 w-4" />
|
||||
<span>Duration</span>
|
||||
<InfoIcon
|
||||
content="Total time from first call to last call completion"
|
||||
side="top"
|
||||
/>
|
||||
</div>
|
||||
<div className="text-xl font-bold text-gray-900 dark:text-white">
|
||||
{(totalDuration / 1000).toFixed(2)}s
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="flex items-center gap-1.5 text-sm text-gray-600 dark:text-gray-400 font-medium mb-2">
|
||||
<DollarSign className="h-4 w-4" />
|
||||
<span>Cost</span>
|
||||
<InfoIcon
|
||||
content="Sum of all LLM call costs in this trace"
|
||||
side="top"
|
||||
/>
|
||||
</div>
|
||||
<div className="text-xl font-bold text-gray-900 dark:text-white">
|
||||
${totalCost.toFixed(4)}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="flex items-center gap-1.5 text-sm text-gray-600 dark:text-gray-400 font-medium mb-2">
|
||||
<Hash className="h-4 w-4" />
|
||||
<span>Tokens</span>
|
||||
<InfoIcon
|
||||
content="Total tokens (prompt + completion) across all calls"
|
||||
side="top"
|
||||
/>
|
||||
</div>
|
||||
<div className="text-xl font-bold text-gray-900 dark:text-white">
|
||||
{totalTokens.toLocaleString()}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="flex items-center gap-1.5 text-sm text-gray-600 dark:text-gray-400 font-medium mb-2">
|
||||
<CodeIcon className="h-4 w-4" />
|
||||
<span>Candidates</span>
|
||||
<InfoIcon
|
||||
content="Number of valid optimization candidates generated"
|
||||
side="top"
|
||||
/>
|
||||
</div>
|
||||
<div className="text-xl font-bold text-gray-900 dark:text-white">
|
||||
{optimizationCandidates.length}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Unified LLM Calls View */}
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg shadow border border-gray-200 dark:border-gray-700 mb-8">
|
||||
<div className="p-6 border-b border-gray-200 dark:border-gray-700">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<h2 className="text-xl font-bold text-gray-900 dark:text-white">LLM Calls</h2>
|
||||
<span className="bg-blue-100 dark:bg-blue-900 text-blue-800 dark:text-blue-200 text-sm px-2.5 py-0.5 rounded font-semibold">
|
||||
{llmCalls.length}
|
||||
</span>
|
||||
</div>
|
||||
<HelpButton
|
||||
title="LLM Calls Timeline"
|
||||
content={
|
||||
<div className="space-y-4 text-sm text-gray-700 dark:text-gray-300">
|
||||
<div>
|
||||
<h4 className="font-semibold text-gray-900 dark:text-white mb-2">
|
||||
Call Sequence
|
||||
</h4>
|
||||
<p>
|
||||
Calls are ordered by sequence number showing the execution order. Each call
|
||||
displays its status, duration, cost, and model used.
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="font-semibold text-gray-900 dark:text-white mb-2">
|
||||
Optimization & Line Profiler
|
||||
</h4>
|
||||
<p>
|
||||
For these call types, generated candidates are displayed directly. Each
|
||||
candidate includes the code and an explanation of the optimization.
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="font-semibold text-gray-900 dark:text-white mb-2">
|
||||
Expanding Calls
|
||||
</h4>
|
||||
<p>
|
||||
Click any call to expand and see detailed metrics including token usage,
|
||||
latency, and timestamp. Use "View full details" to see prompts and responses.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
size="sm"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="divide-y divide-gray-200 dark:divide-gray-700">
|
||||
{orderedTypes.map(callType => {
|
||||
const calls = groupedCalls[callType]
|
||||
const isOptimizationType = callType === "optimization"
|
||||
const isLineProfilerType = callType === "line_profiler"
|
||||
|
||||
// Get the candidates for this call type
|
||||
const candidatesForType = isOptimizationType
|
||||
? optimizationCandidates
|
||||
: isLineProfilerType
|
||||
? lineProfilerCandidates
|
||||
: []
|
||||
|
||||
// For optimization/line_profiler types, only show candidates (not individual LLM calls)
|
||||
const showIndividualCalls = !isOptimizationType && !isLineProfilerType
|
||||
|
||||
// Get the LLM call for this call type to link candidates
|
||||
const llmCallForType = callTypeToLlmCall.get(callType)
|
||||
|
||||
return (
|
||||
<div key={callType} className="p-4">
|
||||
<div className="font-semibold text-gray-900 dark:text-white mb-3 flex items-center gap-2">
|
||||
{callType}
|
||||
{candidatesForType.length > 0 && (
|
||||
<span className="bg-blue-100 dark:bg-blue-900 text-blue-800 dark:text-blue-200 text-xs px-2 py-0.5 rounded font-normal">
|
||||
{candidatesForType.length} candidates
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{showIndividualCalls && (
|
||||
<div className="space-y-2">
|
||||
{calls.map((call, typeIndex) => {
|
||||
const durationSec = ((call.latency_ms || 0) / 1000).toFixed(2)
|
||||
const statusIcon = call.status === "success" ? "✓" : "✗"
|
||||
const callStatusColor =
|
||||
call.status === "success"
|
||||
? "text-green-600 dark:text-green-400"
|
||||
: "text-red-600 dark:text-red-400"
|
||||
const ctx = call.context as { call_sequence?: number } | null
|
||||
const callSequence = ctx?.call_sequence
|
||||
|
||||
return (
|
||||
<details
|
||||
key={call.id}
|
||||
className="group border border-gray-200 dark:border-gray-700 rounded-lg"
|
||||
>
|
||||
<summary className="p-3 cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-700/50 flex items-center justify-between rounded-lg">
|
||||
<div className="flex items-center gap-3 text-sm">
|
||||
<span className="font-mono text-gray-500 dark:text-gray-400 w-8">
|
||||
{callSequence ? `#${callSequence}` : "-"}
|
||||
</span>
|
||||
<span className={`font-mono ${callStatusColor}`}>{statusIcon}</span>
|
||||
<span className="font-medium text-gray-900 dark:text-white">
|
||||
{calls.length > 1 ? `${callType} ${typeIndex + 1}` : callType}
|
||||
</span>
|
||||
<span className="text-gray-500 dark:text-gray-400">
|
||||
{durationSec}s · ${(call.llm_cost || 0).toFixed(4)} ·{" "}
|
||||
{call.model_name}
|
||||
</span>
|
||||
</div>
|
||||
<span className="text-gray-400 group-open:rotate-180 transition-transform">
|
||||
▼
|
||||
</span>
|
||||
</summary>
|
||||
|
||||
<div className="px-4 pb-4 pt-2 border-t border-gray-200 dark:border-gray-700 mt-2">
|
||||
{/* Call details */}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-x-6 gap-y-3 text-sm">
|
||||
<div className="flex flex-col">
|
||||
<span className="text-gray-500 dark:text-gray-400 text-xs mb-1">Tokens</span>
|
||||
<span className="text-gray-900 dark:text-white font-medium">
|
||||
{call.total_tokens?.toLocaleString() ?? "N/A"}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<span className="text-gray-500 dark:text-gray-400 text-xs mb-1">Latency</span>
|
||||
<span className="text-gray-900 dark:text-white font-medium">
|
||||
{call.latency_ms ? `${call.latency_ms}ms` : "N/A"}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<span className="text-gray-500 dark:text-gray-400 text-xs mb-1">Time</span>
|
||||
<span className="text-gray-900 dark:text-white font-medium">
|
||||
{new Date(call.created_at).toLocaleTimeString()}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex flex-col sm:col-span-2 lg:col-span-1">
|
||||
<span className="text-gray-500 dark:text-gray-400 text-xs mb-1">Details</span>
|
||||
<Link
|
||||
href={`/observability/llm-call/${call.id}`}
|
||||
className="text-blue-600 dark:text-blue-400 hover:underline text-sm font-medium inline-block"
|
||||
>
|
||||
View full details →
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
{/* Show candidates for optimization/line_profiler types */}
|
||||
{candidatesForType.length > 0 && (
|
||||
<div className={showIndividualCalls ? "mt-4 ml-4" : ""}>
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<h4 className="text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
Generated Candidates
|
||||
</h4>
|
||||
<InfoIcon
|
||||
content="Code optimization candidates generated from this call. Each includes an explanation and optimized code."
|
||||
side="top"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
{candidatesForType.map(candidate => {
|
||||
const isBest =
|
||||
bestCandidateId != null && candidate.id === bestCandidateId
|
||||
const showUsedForPr = isBest && usedForPr
|
||||
const rank = candidateRankMap.get(candidate.id)
|
||||
return (
|
||||
<details
|
||||
key={candidate.id}
|
||||
className={`rounded-lg border ${
|
||||
isBest
|
||||
? "border-emerald-500 dark:border-emerald-600 bg-emerald-50/50 dark:bg-emerald-900/20"
|
||||
: "border-gray-200 dark:border-gray-600"
|
||||
}`}
|
||||
>
|
||||
<summary className="p-3 cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-700/50 flex items-center justify-between rounded-lg">
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<span className="font-medium text-gray-900 dark:text-white">
|
||||
Candidate {candidate.index}
|
||||
</span>
|
||||
<span className="font-mono text-xs text-gray-500 dark:text-gray-400">
|
||||
{candidate.id.substring(0, 8)}...
|
||||
</span>
|
||||
{rank != null && (
|
||||
<span className="bg-indigo-100 dark:bg-indigo-900 text-indigo-800 dark:text-indigo-200 text-xs px-2 py-0.5 rounded font-semibold">
|
||||
Rank #{rank}
|
||||
</span>
|
||||
)}
|
||||
{isBest && (
|
||||
<span className="bg-emerald-100 dark:bg-emerald-900 text-emerald-800 dark:text-emerald-200 text-xs px-2 py-0.5 rounded font-semibold">
|
||||
Best
|
||||
</span>
|
||||
)}
|
||||
{showUsedForPr && (
|
||||
<span className="bg-blue-100 dark:bg-blue-900 text-blue-800 dark:text-blue-200 text-xs px-2 py-0.5 rounded font-semibold">
|
||||
Used for PR
|
||||
</span>
|
||||
)}
|
||||
{candidate.model && (
|
||||
<span className="bg-purple-100 dark:bg-purple-900 text-purple-800 dark:text-purple-200 text-xs px-2 py-0.5 rounded">
|
||||
{candidate.model}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<span className="text-gray-400 text-sm">▼</span>
|
||||
</summary>
|
||||
<div className="p-3 pt-0 border-t border-gray-200 dark:border-gray-600 mt-2 space-y-3">
|
||||
{candidateExplanations[candidate.id] && (
|
||||
<div>
|
||||
<div className="flex items-center gap-1.5 mb-1">
|
||||
<CodeIcon className="h-3.5 w-3.5 text-gray-500 dark:text-gray-400" />
|
||||
<h5 className="text-xs font-medium text-gray-500 dark:text-gray-400">
|
||||
Explanation
|
||||
</h5>
|
||||
</div>
|
||||
<p className="text-sm text-gray-700 dark:text-gray-300 whitespace-pre-wrap leading-relaxed">
|
||||
{candidateExplanations[candidate.id]}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<h5 className="text-xs font-medium text-gray-500 dark:text-gray-400">
|
||||
Code
|
||||
</h5>
|
||||
<CopyButton text={candidate.code} label="candidate code" size="sm" />
|
||||
</div>
|
||||
<pre className="bg-gray-900 text-gray-100 p-3 rounded-lg overflow-x-auto text-xs font-mono leading-relaxed">
|
||||
<code>{candidate.code}</code>
|
||||
</pre>
|
||||
</div>
|
||||
{llmCallForType && (
|
||||
<div>
|
||||
<Link
|
||||
href={`/observability/llm-call/${llmCallForType.id}`}
|
||||
className="text-blue-600 dark:text-blue-400 hover:underline text-sm font-medium inline-flex items-center gap-1"
|
||||
>
|
||||
View LLM Call Details →
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</details>
|
||||
)})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Ranking explanation — shown when ranker ran */}
|
||||
{rankingData?.explanation && (
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg shadow border border-gray-200 dark:border-gray-700 mb-8">
|
||||
<div className="p-6 border-b border-gray-200 dark:border-gray-700">
|
||||
<div className="flex items-center gap-2">
|
||||
<h2 className="text-xl font-bold text-gray-900 dark:text-white">
|
||||
Ranking explanation
|
||||
</h2>
|
||||
<InfoIcon
|
||||
content="Explanation of why candidates were ranked in this order. Rank numbers are shown on each candidate card."
|
||||
side="top"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-6">
|
||||
<p className="text-sm text-gray-700 dark:text-gray-300 whitespace-pre-wrap leading-relaxed">
|
||||
{rankingData.explanation}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Errors */}
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg shadow border border-gray-200 dark:border-gray-700 mb-8">
|
||||
<div className="p-6 border-b border-gray-200 dark:border-gray-700">
|
||||
<div className="flex items-center gap-3">
|
||||
{errors.length > 0 ? (
|
||||
<>
|
||||
<AlertCircle className="h-5 w-5 text-red-600 dark:text-red-400" />
|
||||
<h2 className="text-xl font-bold text-red-600 dark:text-red-400">
|
||||
Errors
|
||||
</h2>
|
||||
<span className="bg-red-100 dark:bg-red-900 text-red-800 dark:text-red-200 text-sm px-2.5 py-0.5 rounded font-semibold">
|
||||
{errors.length}
|
||||
</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<CheckCircle className="h-5 w-5 text-green-600 dark:text-green-400" />
|
||||
<h2 className="text-xl font-bold text-green-600 dark:text-green-400">
|
||||
No Errors Detected
|
||||
</h2>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{errors.length > 0 ? (
|
||||
<div className="divide-y divide-gray-200 dark:divide-gray-700">
|
||||
{errors.map(error => {
|
||||
const isTestFailure = error.error_type === "test_failure"
|
||||
const errorContext = error.context as
|
||||
| {
|
||||
test_name?: string
|
||||
failure_reason?: string
|
||||
test_output?: string
|
||||
expected?: string
|
||||
actual?: string
|
||||
}
|
||||
| null
|
||||
|
||||
return (
|
||||
<div key={error.id} className="p-6 border-l-4 border-red-500">
|
||||
<div className="flex items-start gap-3">
|
||||
{error.severity === "error" ? (
|
||||
<XCircle className="h-6 w-6 text-red-600 dark:text-red-400 mt-0.5 flex-shrink-0" />
|
||||
) : (
|
||||
<AlertCircle className="h-6 w-6 text-yellow-600 dark:text-yellow-400 mt-0.5 flex-shrink-0" />
|
||||
)}
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<span className="font-semibold text-gray-900 dark:text-white text-base">
|
||||
{error.error_type}
|
||||
</span>
|
||||
<span
|
||||
className={`px-2 py-1 text-xs font-semibold rounded ${
|
||||
error.severity === "error"
|
||||
? "bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-300"
|
||||
: "bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-300"
|
||||
}`}
|
||||
>
|
||||
{error.severity}
|
||||
</span>
|
||||
<span className="text-xs text-gray-500 dark:text-gray-400 ml-auto">
|
||||
{new Date(error.created_at).toLocaleString()}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-start gap-2 mb-3">
|
||||
<p className="text-sm text-gray-700 dark:text-gray-300 flex-1">
|
||||
{error.error_message}
|
||||
</p>
|
||||
<CopyButton text={error.error_message} label="error message" size="sm" />
|
||||
</div>
|
||||
{/* Test Failure Details */}
|
||||
{isTestFailure && errorContext && (
|
||||
<div className="mt-3 p-3 bg-red-50 dark:bg-red-900/20 rounded-lg border border-red-200 dark:border-red-800">
|
||||
<h4 className="text-sm font-semibold text-red-800 dark:text-red-300 mb-2">
|
||||
Test Failure Details
|
||||
</h4>
|
||||
{errorContext.test_name && (
|
||||
<div className="mb-2">
|
||||
<span className="text-xs font-medium text-gray-600 dark:text-gray-400">
|
||||
Test:
|
||||
</span>{" "}
|
||||
<span className="text-sm text-gray-900 dark:text-white font-mono">
|
||||
{errorContext.test_name}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{errorContext.failure_reason && (
|
||||
<div className="mb-2">
|
||||
<span className="text-xs font-medium text-gray-600 dark:text-gray-400">
|
||||
Reason:
|
||||
</span>
|
||||
<p className="text-sm text-gray-900 dark:text-white mt-1 whitespace-pre-wrap">
|
||||
{errorContext.failure_reason}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
{errorContext.expected && (
|
||||
<div className="mb-2">
|
||||
<span className="text-xs font-medium text-gray-600 dark:text-gray-400">
|
||||
Expected:
|
||||
</span>
|
||||
<pre className="text-xs text-gray-900 dark:text-white mt-1 bg-white dark:bg-gray-800 p-2 rounded overflow-x-auto">
|
||||
{String(errorContext.expected)}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
{errorContext.actual && (
|
||||
<div className="mb-2">
|
||||
<span className="text-xs font-medium text-gray-600 dark:text-gray-400">
|
||||
Actual:
|
||||
</span>
|
||||
<pre className="text-xs text-gray-900 dark:text-white mt-1 bg-white dark:bg-gray-800 p-2 rounded overflow-x-auto">
|
||||
{String(errorContext.actual)}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
{errorContext.test_output && (
|
||||
<div>
|
||||
<span className="text-xs font-medium text-gray-600 dark:text-gray-400">
|
||||
Test Output:
|
||||
</span>
|
||||
<pre className="text-xs text-gray-900 dark:text-white mt-1 bg-white dark:bg-gray-800 p-2 rounded overflow-x-auto whitespace-pre-wrap">
|
||||
{String(errorContext.test_output)}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col items-center justify-center py-12 text-center">
|
||||
<CheckCircle className="h-16 w-16 text-green-500 dark:text-green-400 mb-4" />
|
||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-2">
|
||||
All Clear!
|
||||
</h3>
|
||||
<p className="text-gray-500 dark:text-gray-400 text-sm">
|
||||
This trace completed successfully with no errors detected.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
654
js/cf-webapp/src/app/observability/traces/page.tsx
Normal file
654
js/cf-webapp/src/app/observability/traces/page.tsx
Normal file
|
|
@ -0,0 +1,654 @@
|
|||
import Link from "next/link"
|
||||
import { unstable_cache } from "next/cache"
|
||||
import { Search as SearchIcon } from "lucide-react"
|
||||
import { prisma } from "@/lib/prisma"
|
||||
import { safeCostTokens } from "@/lib/observability-utils"
|
||||
import type { Prisma } from "@prisma/client"
|
||||
import { HelpButton } from "@/components/observability/help-button"
|
||||
import { StatCard } from "@/components/observability/stat-card"
|
||||
import { ColumnHeader } from "@/components/observability/column-header"
|
||||
import { InfoIcon } from "@/components/observability/info-icon"
|
||||
|
||||
// Revalidate every 30 seconds
|
||||
export const revalidate = 30
|
||||
|
||||
interface SearchParams {
|
||||
trace_id?: string
|
||||
page?: string
|
||||
organization?: string
|
||||
}
|
||||
|
||||
// Cached function to get unique organizations list
|
||||
// Revalidates every 5 minutes - organizations change infrequently
|
||||
const getUniqueOrganizations = unstable_cache(
|
||||
async () => {
|
||||
const uniqueOrganizations = await prisma.optimization_features.findMany({
|
||||
select: { organization: true },
|
||||
distinct: ["organization"],
|
||||
where: { organization: { not: null } },
|
||||
})
|
||||
return uniqueOrganizations.map(f => f.organization).filter(Boolean).sort() as string[]
|
||||
},
|
||||
["unique-organizations"],
|
||||
{ revalidate: 300 }, // 5 minutes
|
||||
)
|
||||
|
||||
// Optimized function to count distinct trace_ids using groupBy
|
||||
const getTotalTracesCount = unstable_cache(
|
||||
async (traceIdFilter: string | undefined, organizationFilter: string | undefined) => {
|
||||
// Get trace IDs filtered by organization if specified
|
||||
let traceIdPrefixes: string[] = []
|
||||
if (organizationFilter) {
|
||||
const orgFeatures = await prisma.optimization_features.findMany({
|
||||
where: { organization: organizationFilter },
|
||||
select: { trace_id: true },
|
||||
distinct: ["trace_id"],
|
||||
})
|
||||
traceIdPrefixes = orgFeatures.map(f => f.trace_id).filter(Boolean) as string[]
|
||||
if (traceIdPrefixes.length === 0) return 0
|
||||
}
|
||||
|
||||
// Build where clause
|
||||
const where: Prisma.llm_callsWhereInput = {}
|
||||
|
||||
if (traceIdFilter) {
|
||||
where.trace_id = { contains: traceIdFilter }
|
||||
} else if (traceIdPrefixes.length > 0) {
|
||||
// Use IN clause for exact trace ID matches - much more efficient than OR with startsWith
|
||||
where.trace_id = { in: traceIdPrefixes }
|
||||
}
|
||||
|
||||
// Use groupBy for efficient distinct count
|
||||
// Filter out null trace_ids in the result
|
||||
const result = await prisma.llm_calls.groupBy({
|
||||
by: ["trace_id"],
|
||||
where,
|
||||
})
|
||||
|
||||
// Filter out null trace_ids
|
||||
return result.filter(r => r.trace_id !== null).length
|
||||
},
|
||||
["traces-count"],
|
||||
{ revalidate: 30 },
|
||||
)
|
||||
|
||||
export default async function TracesPage({ searchParams }: { searchParams: SearchParams }) {
|
||||
try {
|
||||
const page = parseInt(searchParams.page || "1")
|
||||
const pageSize = 50
|
||||
const skip = (page - 1) * pageSize
|
||||
|
||||
// Get trace IDs filtered by organization if specified
|
||||
let filteredTraceIds: string[] = []
|
||||
if (searchParams.organization) {
|
||||
const orgFeatures = await prisma.optimization_features.findMany({
|
||||
where: { organization: searchParams.organization },
|
||||
select: { trace_id: true },
|
||||
distinct: ["trace_id"],
|
||||
})
|
||||
filteredTraceIds = orgFeatures.map(f => f.trace_id).filter(Boolean) as string[]
|
||||
|
||||
// If organization filter is applied but no traces found, return empty result early
|
||||
if (filteredTraceIds.length === 0) {
|
||||
const uniqueOrganizations = await prisma.optimization_features.findMany({
|
||||
select: { organization: true },
|
||||
distinct: ["organization"],
|
||||
where: { organization: { not: null } },
|
||||
})
|
||||
const orgs = uniqueOrganizations.map(f => f.organization).filter(Boolean).sort() as string[]
|
||||
|
||||
// Return early with empty results
|
||||
return (
|
||||
<div className="container mx-auto p-6">
|
||||
{/* Header with search form */}
|
||||
<div className="mb-8">
|
||||
<div className="flex items-center justify-between gap-4 mb-2">
|
||||
<div className="flex items-center gap-3">
|
||||
<h2 className="text-2xl font-bold tracking-tight text-gray-900 dark:text-white whitespace-nowrap">
|
||||
All Traces
|
||||
</h2>
|
||||
<HelpButton
|
||||
title="Understanding Traces"
|
||||
content={
|
||||
<div className="space-y-4 text-sm text-gray-700 dark:text-gray-300">
|
||||
<div>
|
||||
<h4 className="font-semibold text-gray-900 dark:text-white mb-2">What is a trace?</h4>
|
||||
<p>A trace represents a complete optimization request from start to finish. Each trace contains all the LLM API calls made during that optimization.</p>
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="font-semibold text-gray-900 dark:text-white mb-2">Multi-model traces</h4>
|
||||
<p>When using multiple models for optimization, all calls share the same base trace_id (first 33 characters). This helps track related operations together.</p>
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="font-semibold text-gray-900 dark:text-white mb-2">Page sections</h4>
|
||||
<ul className="list-disc list-inside space-y-1 ml-2">
|
||||
<li><strong>Summary Stats:</strong> Quick overview of trace metrics on this page</li>
|
||||
<li><strong>Traces Table:</strong> Detailed list of all traces with aggregated data</li>
|
||||
<li><strong>Filters:</strong> Search by trace ID or filter by organization</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<form method="get" className="flex items-center gap-2 flex-1 max-w-2xl">
|
||||
<div className="flex-1 relative">
|
||||
<input
|
||||
type="text"
|
||||
name="trace_id"
|
||||
placeholder="Search by Trace ID..."
|
||||
defaultValue={searchParams.trace_id || ""}
|
||||
className="w-full px-3 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 placeholder-gray-500 dark:placeholder-gray-400 focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-shadow"
|
||||
title="Search by full or partial trace ID. Finds all related calls."
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<label htmlFor="organization-filter-empty" className="text-sm font-medium text-gray-700 dark:text-gray-300 whitespace-nowrap">
|
||||
Organization
|
||||
</label>
|
||||
<InfoIcon
|
||||
content="Filter by organization that initiated the optimization request"
|
||||
side="top"
|
||||
/>
|
||||
</div>
|
||||
<select
|
||||
id="organization-filter-empty"
|
||||
name="organization"
|
||||
defaultValue={searchParams.organization || ""}
|
||||
className="px-3 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||||
>
|
||||
<option value="">All Organizations</option>
|
||||
{orgs.map(org => (
|
||||
<option key={org} value={org}>
|
||||
{org}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<button
|
||||
type="submit"
|
||||
className="px-4 py-2 text-sm bg-blue-600 dark:bg-blue-700 text-white rounded hover:bg-blue-700 dark:hover:bg-blue-600 whitespace-nowrap"
|
||||
>
|
||||
Search
|
||||
</button>
|
||||
<Link
|
||||
href="/observability/traces"
|
||||
className="px-4 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded hover:bg-gray-50 dark:hover:bg-gray-700 text-gray-900 dark:text-gray-100 whitespace-nowrap"
|
||||
>
|
||||
Clear
|
||||
</Link>
|
||||
</form>
|
||||
</div>
|
||||
<p className="text-gray-600 dark:text-gray-400">
|
||||
View optimization request traces with aggregated metrics
|
||||
</p>
|
||||
</div>
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg shadow p-16 text-center border border-gray-200 dark:border-gray-700">
|
||||
<div className="flex flex-col items-center justify-center space-y-4">
|
||||
<SearchIcon className="h-12 w-12 text-gray-400 dark:text-gray-500" />
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-1">
|
||||
No Traces Found
|
||||
</h3>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">
|
||||
No traces found for organization "{searchParams.organization}". Try selecting a different organization.
|
||||
</p>
|
||||
</div>
|
||||
<Link
|
||||
href="/observability/llm-calls"
|
||||
className="text-sm text-blue-600 dark:text-blue-400 hover:underline"
|
||||
>
|
||||
View All LLM Calls →
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Build where clause for LLM calls with organization filter applied at database level
|
||||
// NOTE: Filtering happens at DB level BEFORE pagination, not client-side.
|
||||
// We use IN clause because there's no Prisma relation between llm_calls and optimization_features
|
||||
// (they're only related by trace_id as a string field, not a foreign key relation)
|
||||
const where: Prisma.llm_callsWhereInput = {}
|
||||
|
||||
if (searchParams.trace_id) {
|
||||
where.trace_id = { contains: searchParams.trace_id }
|
||||
} else if (filteredTraceIds.length > 0) {
|
||||
// Use IN clause for exact trace ID matches - much more efficient than OR with startsWith
|
||||
// For very large organizations (>10k traces), consider chunking the array
|
||||
where.trace_id = { in: filteredTraceIds }
|
||||
}
|
||||
|
||||
// STEP 1: Get distinct trace_ids with pagination using groupBy
|
||||
const [distinctTraces, totalTracesCount] = await Promise.all([
|
||||
prisma.llm_calls.groupBy({
|
||||
by: ["trace_id"],
|
||||
where,
|
||||
orderBy: { _max: { created_at: "desc" } },
|
||||
take: pageSize,
|
||||
skip,
|
||||
_max: { created_at: true },
|
||||
}),
|
||||
getTotalTracesCount(searchParams.trace_id, searchParams.organization),
|
||||
])
|
||||
|
||||
// Extract trace_ids from the paginated results
|
||||
const paginatedTraceIds = distinctTraces
|
||||
.map(t => t.trace_id)
|
||||
.filter(Boolean) as string[]
|
||||
|
||||
// STEP 2: Fetch all LLM calls ONLY for the paginated trace_ids
|
||||
const llmCallsRaw = paginatedTraceIds.length > 0
|
||||
? await prisma.llm_calls.findMany({
|
||||
where: { trace_id: { in: paginatedTraceIds } },
|
||||
orderBy: { created_at: "desc" },
|
||||
select: {
|
||||
trace_id: true,
|
||||
created_at: true,
|
||||
llm_cost: true,
|
||||
total_tokens: true,
|
||||
status: true,
|
||||
call_type: true,
|
||||
},
|
||||
})
|
||||
: []
|
||||
|
||||
// Filter out null trace_ids
|
||||
const llmCalls = llmCallsRaw.filter(call => call.trace_id !== null)
|
||||
|
||||
// Fetch organizations ONLY for the paginated trace_ids
|
||||
const [allOptimizationFeatures] = await Promise.all([
|
||||
paginatedTraceIds.length > 0
|
||||
? prisma.optimization_features.findMany({
|
||||
where: { trace_id: { in: paginatedTraceIds } },
|
||||
select: {
|
||||
trace_id: true,
|
||||
organization: true,
|
||||
},
|
||||
})
|
||||
: [],
|
||||
])
|
||||
|
||||
// Create a map of trace_id to organization
|
||||
const traceIdToOrganization = new Map<string, string>()
|
||||
allOptimizationFeatures.forEach(feature => {
|
||||
if (feature.trace_id && feature.organization) {
|
||||
traceIdToOrganization.set(feature.trace_id, feature.organization)
|
||||
}
|
||||
})
|
||||
|
||||
// Get unique organizations for filter dropdown (cached)
|
||||
const orgs = await getUniqueOrganizations()
|
||||
|
||||
// Group by trace_id and calculate aggregates
|
||||
const traceMap = new Map<
|
||||
string,
|
||||
{
|
||||
trace_id: string
|
||||
first_seen: Date
|
||||
last_seen: Date
|
||||
call_count: number
|
||||
total_cost: number
|
||||
total_tokens: number
|
||||
failed_calls: number
|
||||
status: string
|
||||
call_types: Set<string>
|
||||
}
|
||||
>()
|
||||
|
||||
llmCalls.forEach(call => {
|
||||
if (!call.trace_id) return
|
||||
|
||||
const callTimestamp = new Date(call.created_at).getTime()
|
||||
const existing = traceMap.get(call.trace_id)
|
||||
const { cost: callCost, tokens: callTokens } = safeCostTokens(call.llm_cost, call.total_tokens)
|
||||
|
||||
if (existing) {
|
||||
existing.call_count++
|
||||
existing.total_cost += callCost
|
||||
existing.total_tokens += callTokens
|
||||
if (call.status === "failed") existing.failed_calls++
|
||||
if (call.call_type) existing.call_types.add(call.call_type)
|
||||
// Use Math.min/Math.max to ensure correct first/last timestamps
|
||||
existing.first_seen = new Date(Math.min(existing.first_seen.getTime(), callTimestamp))
|
||||
existing.last_seen = new Date(Math.max(existing.last_seen.getTime(), callTimestamp))
|
||||
} else {
|
||||
traceMap.set(call.trace_id, {
|
||||
trace_id: call.trace_id,
|
||||
first_seen: new Date(callTimestamp),
|
||||
last_seen: new Date(callTimestamp),
|
||||
call_count: 1,
|
||||
total_cost: callCost,
|
||||
total_tokens: callTokens,
|
||||
failed_calls: call.status === "failed" ? 1 : 0,
|
||||
status: call.status || "unknown",
|
||||
call_types: new Set(call.call_type ? [call.call_type] : []),
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// Convert to array and sort by last_seen desc
|
||||
// No need to paginate here - already done at database level
|
||||
const traces = Array.from(traceMap.values()).sort(
|
||||
(a, b) => b.last_seen.getTime() - a.last_seen.getTime(),
|
||||
)
|
||||
const totalPages = Math.ceil(totalTracesCount / pageSize)
|
||||
|
||||
return (
|
||||
<div className="container mx-auto p-6">
|
||||
{/* Header */}
|
||||
<div className="mb-8">
|
||||
{/* Title, Search Bar, and Help Button */}
|
||||
<div className="flex items-center justify-between gap-4 mb-2">
|
||||
<div className="flex items-center gap-3">
|
||||
<h2 className="text-2xl font-bold tracking-tight text-gray-900 dark:text-white whitespace-nowrap">
|
||||
All Traces
|
||||
</h2>
|
||||
<HelpButton
|
||||
title="Understanding Traces"
|
||||
content={
|
||||
<div className="space-y-4 text-sm text-gray-700 dark:text-gray-300">
|
||||
<div>
|
||||
<h4 className="font-semibold text-gray-900 dark:text-white mb-2">What is a trace?</h4>
|
||||
<p>A trace represents a complete optimization request from start to finish. Each trace contains all the LLM API calls made during that optimization.</p>
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="font-semibold text-gray-900 dark:text-white mb-2">Multi-model traces</h4>
|
||||
<p>When using multiple models for optimization, all calls share the same base trace_id (first 33 characters). This helps track related operations together.</p>
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="font-semibold text-gray-900 dark:text-white mb-2">Page sections</h4>
|
||||
<ul className="list-disc list-inside space-y-1 ml-2">
|
||||
<li><strong>Summary Stats:</strong> Quick overview of trace metrics on this page</li>
|
||||
<li><strong>Traces Table:</strong> Detailed list of all traces with aggregated data</li>
|
||||
<li><strong>Filters:</strong> Search by trace ID or filter by organization</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
{/* Compact Search Bar */}
|
||||
<form method="get" className="flex items-center gap-2 flex-1 max-w-2xl">
|
||||
<div className="flex-1 relative">
|
||||
<input
|
||||
type="text"
|
||||
name="trace_id"
|
||||
placeholder="Search by Trace ID..."
|
||||
defaultValue={searchParams.trace_id || ""}
|
||||
className="w-full px-3 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 placeholder-gray-500 dark:placeholder-gray-400 focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-shadow"
|
||||
title="Search by full or partial trace ID. Finds all related calls."
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<label htmlFor="organization-filter" className="text-sm font-medium text-gray-700 dark:text-gray-300 whitespace-nowrap">
|
||||
Organization
|
||||
</label>
|
||||
<InfoIcon
|
||||
content="Filter by organization that initiated the optimization request"
|
||||
side="top"
|
||||
/>
|
||||
</div>
|
||||
<select
|
||||
id="organization-filter"
|
||||
name="organization"
|
||||
defaultValue={searchParams.organization || ""}
|
||||
className="px-3 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||||
>
|
||||
<option value="">All Organizations</option>
|
||||
{orgs.map(org => (
|
||||
<option key={org} value={org}>
|
||||
{org}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<button
|
||||
type="submit"
|
||||
className="px-4 py-2 text-sm bg-blue-600 dark:bg-blue-700 text-white rounded hover:bg-blue-700 dark:hover:bg-blue-600 whitespace-nowrap"
|
||||
>
|
||||
Search
|
||||
</button>
|
||||
{(searchParams.trace_id || searchParams.organization) && (
|
||||
<Link
|
||||
href="/observability/traces"
|
||||
className="px-4 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded hover:bg-gray-50 dark:hover:bg-gray-700 text-gray-900 dark:text-gray-100 whitespace-nowrap"
|
||||
>
|
||||
Clear
|
||||
</Link>
|
||||
)}
|
||||
</form>
|
||||
</div>
|
||||
<p className="text-gray-600 dark:text-gray-400">
|
||||
View optimization request traces with aggregated metrics
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Summary Stats */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-5 mb-8">
|
||||
<StatCard
|
||||
label="Total Traces"
|
||||
value={totalTracesCount}
|
||||
helpText="Total number of unique traces across all pages matching your filters"
|
||||
icon="Database"
|
||||
/>
|
||||
<StatCard
|
||||
label="LLM Calls (Page)"
|
||||
value={llmCalls.length}
|
||||
helpText="Number of individual LLM API calls on this page. Each trace may have multiple calls."
|
||||
icon="Zap"
|
||||
variant="default"
|
||||
className="border-l-4 border-blue-500"
|
||||
/>
|
||||
<StatCard
|
||||
label="Total Cost (Page)"
|
||||
value={`$${traces.reduce((sum, t) => sum + t.total_cost, 0).toFixed(4)}`}
|
||||
helpText="Total cost in USD for all LLM calls shown on this page. Based on model pricing and token usage."
|
||||
icon="DollarSign"
|
||||
/>
|
||||
<StatCard
|
||||
label="Failed Traces (Page)"
|
||||
value={traces.filter(t => t.failed_calls > 0).length}
|
||||
helpText="Traces containing at least one failed LLM call. Click a trace to see error details."
|
||||
icon="AlertTriangle"
|
||||
variant="error"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Traces Table */}
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg shadow border border-gray-200 dark:border-gray-700">
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full">
|
||||
<thead className="bg-gray-50 dark:bg-gray-900">
|
||||
<tr>
|
||||
<ColumnHeader
|
||||
label="Trace ID"
|
||||
tooltip="Unique identifier for this optimization request. Click to view detailed timeline."
|
||||
/>
|
||||
<ColumnHeader
|
||||
label="Organization"
|
||||
tooltip="Organization that initiated this optimization (from GitHub/CLI metadata)"
|
||||
/>
|
||||
<ColumnHeader
|
||||
label="Status"
|
||||
tooltip="Overall status: Success (all calls succeeded), Partial (some succeeded), Failed (any call failed)"
|
||||
/>
|
||||
<ColumnHeader
|
||||
label="Calls"
|
||||
tooltip="Number of LLM API calls in this trace. Includes retries and multi-model attempts."
|
||||
/>
|
||||
<ColumnHeader
|
||||
label="Call Types"
|
||||
tooltip="Types of LLM operations: optimization, validation, line_profiler, etc."
|
||||
/>
|
||||
<ColumnHeader
|
||||
label="Cost"
|
||||
tooltip="Total cost in USD for all LLM calls in this trace"
|
||||
/>
|
||||
<ColumnHeader
|
||||
label="Tokens"
|
||||
tooltip="Combined prompt + completion tokens across all calls"
|
||||
/>
|
||||
<ColumnHeader
|
||||
label="Duration"
|
||||
tooltip="Time between first and last call in this trace"
|
||||
/>
|
||||
<ColumnHeader
|
||||
label="Last Seen"
|
||||
tooltip="Timestamp of the most recent call in this trace"
|
||||
/>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-gray-200 dark:divide-gray-700">
|
||||
{traces.length === 0 ? (
|
||||
<tr>
|
||||
<td
|
||||
colSpan={9}
|
||||
className="px-6 py-16 text-center"
|
||||
>
|
||||
<div className="flex flex-col items-center justify-center space-y-4">
|
||||
<SearchIcon className="h-12 w-12 text-gray-400 dark:text-gray-500" />
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-1">
|
||||
No Traces Found
|
||||
</h3>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">
|
||||
{searchParams.trace_id || searchParams.organization
|
||||
? "Try adjusting your filters above"
|
||||
: "Run an optimization to see traces here"}
|
||||
</p>
|
||||
</div>
|
||||
{(searchParams.trace_id || searchParams.organization) && (
|
||||
<Link
|
||||
href="/observability/llm-calls"
|
||||
className="text-sm text-blue-600 dark:text-blue-400 hover:underline"
|
||||
>
|
||||
View All LLM Calls →
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
) : (
|
||||
traces.map(trace => {
|
||||
// Ensure duration is never negative by using Math.max
|
||||
const duration = Math.max(
|
||||
0,
|
||||
(trace.last_seen.getTime() - trace.first_seen.getTime()) / 1000,
|
||||
)
|
||||
// Determine status: check if any call has partial_success, failed, or all success
|
||||
const hasPartial =
|
||||
trace.status === "partial_success" ||
|
||||
llmCalls.some(c => c.trace_id === trace.trace_id && c.status === "partial_success")
|
||||
const statusColor =
|
||||
trace.failed_calls > 0
|
||||
? "bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-300"
|
||||
: hasPartial
|
||||
? "bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-300"
|
||||
: "bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-300"
|
||||
const statusText = trace.failed_calls > 0 ? "Failed" : hasPartial ? "Partial" : "Success"
|
||||
const statusBorderColor = trace.failed_calls > 0 ? "border-red-500" : hasPartial ? "border-yellow-500" : "border-green-500"
|
||||
|
||||
return (
|
||||
<tr
|
||||
key={trace.trace_id}
|
||||
className={`border-l-4 border-transparent hover:bg-gray-50 dark:hover:bg-gray-700/50 hover:shadow-md hover:${statusBorderColor} transition-all duration-200`}
|
||||
>
|
||||
<td className="px-6 py-5">
|
||||
{trace.trace_id && trace.trace_id.trim() ? (
|
||||
<Link
|
||||
href={`/observability/trace/${trace.trace_id}`}
|
||||
className="text-blue-600 dark:text-blue-400 hover:underline font-mono text-sm"
|
||||
>
|
||||
{trace.trace_id.substring(0, 8)}...
|
||||
</Link>
|
||||
) : (
|
||||
<span className="text-gray-400 dark:text-gray-500 font-mono text-sm">N/A</span>
|
||||
)}
|
||||
</td>
|
||||
<td className="px-6 py-5 whitespace-nowrap text-sm text-gray-700 dark:text-gray-300">
|
||||
{traceIdToOrganization.get(trace.trace_id) || "N/A"}
|
||||
</td>
|
||||
<td className="px-6 py-5 whitespace-nowrap">
|
||||
<span className={`inline-flex px-2 py-1 text-xs font-semibold rounded-full ${statusColor}`}>
|
||||
{statusText}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-6 py-5 whitespace-nowrap text-sm text-gray-700 dark:text-gray-300">
|
||||
{trace.call_count}
|
||||
{trace.failed_calls > 0 && (
|
||||
<span className="text-red-600 dark:text-red-400 ml-1">
|
||||
({trace.failed_calls} failed)
|
||||
</span>
|
||||
)}
|
||||
</td>
|
||||
<td className="px-6 py-5 text-sm text-gray-700 dark:text-gray-300">
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{Array.from(trace.call_types)
|
||||
.slice(0, 3)
|
||||
.map(type => (
|
||||
<span
|
||||
key={type}
|
||||
className="px-2 py-0.5 text-xs bg-gray-100 dark:bg-gray-700 rounded"
|
||||
>
|
||||
{type}
|
||||
</span>
|
||||
))}
|
||||
{trace.call_types.size > 3 && (
|
||||
<span className="px-2 py-0.5 text-xs text-gray-500 dark:text-gray-400">
|
||||
+{trace.call_types.size - 3}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-6 py-5 whitespace-nowrap text-sm text-gray-700 dark:text-gray-300">
|
||||
${trace.total_cost.toFixed(4)}
|
||||
</td>
|
||||
<td className="px-6 py-5 whitespace-nowrap text-sm text-gray-700 dark:text-gray-300">
|
||||
{trace.total_tokens.toLocaleString()}
|
||||
</td>
|
||||
<td className="px-6 py-5 whitespace-nowrap text-sm text-gray-700 dark:text-gray-300">
|
||||
{duration.toFixed(2)}s
|
||||
</td>
|
||||
<td className="px-6 py-5 whitespace-nowrap text-sm text-gray-600 dark:text-gray-400">
|
||||
{trace.last_seen.toLocaleString()}
|
||||
</td>
|
||||
</tr>
|
||||
)
|
||||
})
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Pagination */}
|
||||
{totalPages > 1 && (
|
||||
<div className="mt-8 flex justify-center items-center gap-4">
|
||||
{page > 1 && (
|
||||
<Link
|
||||
href={`/observability/traces?page=${page - 1}${searchParams.trace_id ? `&trace_id=${searchParams.trace_id}` : ""}${searchParams.organization ? `&organization=${searchParams.organization}` : ""}`}
|
||||
className="px-4 py-2 border border-gray-300 dark:border-gray-600 rounded hover:bg-gray-50 dark:hover:bg-gray-700 text-gray-900 dark:text-gray-100"
|
||||
>
|
||||
Previous
|
||||
</Link>
|
||||
)}
|
||||
<span className="text-gray-600 dark:text-gray-400">
|
||||
Page {page} of {totalPages} • Showing {traces.length} traces
|
||||
</span>
|
||||
{page < totalPages && (
|
||||
<Link
|
||||
href={`/observability/traces?page=${page + 1}${searchParams.trace_id ? `&trace_id=${searchParams.trace_id}` : ""}${searchParams.organization ? `&organization=${searchParams.organization}` : ""}`}
|
||||
className="px-4 py-2 border border-gray-300 dark:border-gray-600 rounded hover:bg-gray-50 dark:hover:bg-gray-700 text-gray-900 dark:text-gray-100"
|
||||
>
|
||||
Next
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
} catch (error) {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
174
js/cf-webapp/src/app/trace/[trace_id]/page.tsx
Normal file
174
js/cf-webapp/src/app/trace/[trace_id]/page.tsx
Normal file
|
|
@ -0,0 +1,174 @@
|
|||
import { PrismaClient } from "@prisma/client"
|
||||
import { notFound } from "next/navigation"
|
||||
import Link from "next/link"
|
||||
import { ExperimentMetadata } from "@/lib/types" // Your defined types
|
||||
import MonacoDiffViewer from "@/components/trace/monaco-diff-viewer" // The client component
|
||||
import { Metadata } from "next" // For Next.js metadata API
|
||||
import { getSession } from "@auth0/nextjs-auth0"
|
||||
import { isTeamMember } from "@/app/utils/auth"
|
||||
|
||||
interface TraceDetailsPageProps {
|
||||
params: {
|
||||
trace_id: string
|
||||
}
|
||||
}
|
||||
const prisma = new PrismaClient()
|
||||
// Function to generate dynamic metadata (e.g., page title)
|
||||
export async function generateMetadata({ params }: TraceDetailsPageProps): Promise<Metadata> {
|
||||
const { trace_id } = params
|
||||
|
||||
// Optionally fetch minimal data for title generation to avoid over-fetching
|
||||
// For simplicity, we'll use a generic title or one derived if data is fetched quickly
|
||||
// A more optimized approach might involve a separate lightweight query or using default values.
|
||||
|
||||
const optimizationFeature = await prisma.optimization_features.findUnique({
|
||||
where: { trace_id },
|
||||
select: {
|
||||
experiment_metadata: true,
|
||||
organization: true,
|
||||
repository: true,
|
||||
review_quality: true,
|
||||
review_explanation: true,
|
||||
},
|
||||
})
|
||||
|
||||
let title = `Python Diff Trace: ${trace_id.substring(0, 8)}`
|
||||
if (optimizationFeature?.experiment_metadata) {
|
||||
const metadata = optimizationFeature.experiment_metadata as unknown as ExperimentMetadata // Type assertion
|
||||
const repoName =
|
||||
optimizationFeature.organization && optimizationFeature.repository
|
||||
? `${optimizationFeature.organization}/${optimizationFeature.repository}`
|
||||
: metadata.owner && metadata.repo
|
||||
? `${metadata.owner}/${metadata.repo}`
|
||||
: ""
|
||||
|
||||
if (metadata.prCommentFields?.function_name) {
|
||||
title = `Diff: ${metadata.prCommentFields.function_name} (${repoName})`
|
||||
} else if (repoName) {
|
||||
title = `Diff: ${repoName} - Trace ${trace_id.substring(0, 8)}`
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
title: `${title} | Codeflash AI`,
|
||||
description: `Review CodeFlash Python code optimization diffs for trace ID ${trace_id}.`,
|
||||
// You can add more OpenGraph tags, etc.
|
||||
}
|
||||
}
|
||||
|
||||
// The main page component
|
||||
export default async function TraceDetailsPage({ params }: TraceDetailsPageProps) {
|
||||
const { trace_id } = params
|
||||
|
||||
if (!trace_id) {
|
||||
// This case should ideally be handled by Next.js routing if trace_id is missing in URL structure
|
||||
notFound()
|
||||
}
|
||||
|
||||
const session = await getSession()
|
||||
if (!session?.user) return null
|
||||
|
||||
// Check team member access - only team members can view traces
|
||||
const hasTeamAccess = await isTeamMember()
|
||||
if (!hasTeamAccess) {
|
||||
// Create a custom access denied page or redirect to a generic error
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center h-screen bg-gradient-to-br from-slate-900 via-slate-800 to-slate-900 text-slate-200">
|
||||
<div className="text-center max-w-md">
|
||||
<div className="mb-6">
|
||||
<svg
|
||||
className="w-16 h-16 mx-auto text-red-400"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L3.732 16.5c-.77.833.192 2.5 1.732 2.5z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<h1 className="text-2xl font-bold text-red-400 mb-4">Access Denied</h1>
|
||||
<p className="text-slate-300 mb-6">
|
||||
This trace is restricted to CodeFlash team members only.
|
||||
</p>
|
||||
<div className="text-sm text-slate-400 mb-6">
|
||||
<p>
|
||||
Logged in as:{" "}
|
||||
<span className="font-mono">{session.user.email || session.user.nickname}</span>
|
||||
</p>
|
||||
<p>
|
||||
Trace ID: <span className="font-mono">{trace_id}</span>
|
||||
</p>
|
||||
</div>
|
||||
<Link
|
||||
href="/app"
|
||||
className="inline-block bg-blue-600 hover:bg-blue-700 text-white px-6 py-2 rounded-lg transition-colors"
|
||||
>
|
||||
Go to Dashboard
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
let optimizationFeature: {
|
||||
experiment_metadata: unknown
|
||||
metadata: unknown
|
||||
organization: string | null
|
||||
repository: string | null
|
||||
review_quality: string | null
|
||||
review_explanation: string | null
|
||||
} | null = null
|
||||
try {
|
||||
optimizationFeature = await prisma.optimization_features.findUnique({
|
||||
where: { trace_id: trace_id },
|
||||
select: {
|
||||
experiment_metadata: true, // Prisma handles JSONB parsing
|
||||
metadata: true, // Include metadata field which stores modified code
|
||||
organization: true,
|
||||
repository: true,
|
||||
review_quality: true,
|
||||
review_explanation: true,
|
||||
// Select other fields if needed by MonacoDiffViewer for its header/display
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
console.error(`[TracePage] Failed to fetch data for trace_id ${trace_id}:`, error)
|
||||
// Optionally, render a specific error UI component here instead of notFound()
|
||||
// For now, notFound() will trigger the 404 page, which is reasonable if data fetch fails badly.
|
||||
// Or you could pass an error state to MonacoDiffViewer to display.
|
||||
// For this detailed guide, we assume MonacoDiffViewer will handle 'null' metadata.
|
||||
}
|
||||
|
||||
// If feature is not found, or metadata is explicitly null (and you expect it for valid traces)
|
||||
if (!optimizationFeature) {
|
||||
notFound() // Triggers the Next.js 404 page
|
||||
}
|
||||
|
||||
// Type assertion is safe here due to the check above or if your DB guarantees metadata for valid traces.
|
||||
// If experiment_metadata can be legitimately null for an existing trace_id, handle it gracefully.
|
||||
// Pass experiment metadata directly since modifications are now stored in diffContents
|
||||
const metadata = optimizationFeature.experiment_metadata as ExperimentMetadata | null
|
||||
const review_quality = optimizationFeature.review_quality as string | null
|
||||
const review_explanation = optimizationFeature.review_explanation as string | null
|
||||
// Determine repository full name for display
|
||||
const repoFullName =
|
||||
optimizationFeature.organization && optimizationFeature.repository
|
||||
? `${optimizationFeature.organization}/${optimizationFeature.repository}`
|
||||
: metadata?.owner && metadata?.repo
|
||||
? `${metadata.owner}/${metadata.repo}`
|
||||
: "N/A"
|
||||
|
||||
return (
|
||||
<MonacoDiffViewer
|
||||
metadata={metadata} // Pass the potentially null metadata; component handles it
|
||||
repoFullName={repoFullName}
|
||||
traceId={trace_id}
|
||||
review_quality={review_quality || ""}
|
||||
review_explanation={review_explanation || ""}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
|
@ -23,6 +23,7 @@ export function ConditionalLayout({
|
|||
const shouldHideLayout =
|
||||
pathname !== null && (
|
||||
HIDDEN_PAGES.includes(pathname) ||
|
||||
pathname.startsWith("/trace/") ||
|
||||
pathname.startsWith("/observability") ||
|
||||
!user
|
||||
)
|
||||
|
|
|
|||
|
|
@ -301,7 +301,7 @@ export function Sidebar({ className, user, isLoading, error }: SidebarProps): JS
|
|||
|
||||
{/* Observability Group */}
|
||||
<Link
|
||||
href="/observability"
|
||||
href="/observability/traces"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="block mb-1"
|
||||
|
|
|
|||
|
|
@ -1,378 +0,0 @@
|
|||
"use client"
|
||||
|
||||
import { useState, useMemo, useCallback, memo } from "react"
|
||||
import { ChevronDown, Code, FileText, Hash, FolderTree } from "lucide-react"
|
||||
import { CodeHighlighter, CODE_STYLE } from "./code-highlighter"
|
||||
import { CopyButton } from "./copy-button"
|
||||
import { InfoIcon } from "./info-icon"
|
||||
|
||||
interface CodeContextSectionProps {
|
||||
functionName: string | null
|
||||
filePath: string | null
|
||||
originalCode: string | null
|
||||
dependencyCode: string | null
|
||||
}
|
||||
|
||||
interface ParsedFile {
|
||||
path: string
|
||||
filename: string
|
||||
language: string
|
||||
code: string
|
||||
tokens: number
|
||||
}
|
||||
|
||||
function estimateTokens(text: string): number {
|
||||
return Math.ceil(text.length / 4)
|
||||
}
|
||||
|
||||
function getFilename(path: string): string {
|
||||
return path.split("/").pop() || path
|
||||
}
|
||||
|
||||
function parseMarkdownCodeBlocks(markdown: string): ParsedFile[] {
|
||||
const files: ParsedFile[] = []
|
||||
const regex = /```(\w+):([^\n]+)\n([\s\S]*?)```/g
|
||||
let match
|
||||
|
||||
while ((match = regex.exec(markdown)) !== null) {
|
||||
const [, language, path, code] = match
|
||||
files.push({
|
||||
path,
|
||||
filename: getFilename(path),
|
||||
language: language || "python",
|
||||
code: code.trimEnd(),
|
||||
tokens: estimateTokens(code),
|
||||
})
|
||||
}
|
||||
|
||||
return files
|
||||
}
|
||||
|
||||
export const CodeContextSection = memo(function CodeContextSection({
|
||||
functionName,
|
||||
filePath,
|
||||
originalCode,
|
||||
dependencyCode,
|
||||
}: CodeContextSectionProps) {
|
||||
const [isExpanded, setIsExpanded] = useState(false)
|
||||
const [expandedSections, setExpandedSections] = useState<Set<string>>(new Set())
|
||||
const [expandedFiles, setExpandedFiles] = useState<Set<string>>(new Set())
|
||||
|
||||
const { rwFiles, roFiles, metrics } = useMemo(() => {
|
||||
const rwFiles = originalCode ? parseMarkdownCodeBlocks(originalCode) : []
|
||||
const roFiles = dependencyCode ? parseMarkdownCodeBlocks(dependencyCode) : []
|
||||
const rwTokens = rwFiles.reduce((sum, f) => sum + f.tokens, 0)
|
||||
const roTokens = roFiles.reduce((sum, f) => sum + f.tokens, 0)
|
||||
const totalFiles = rwFiles.length + roFiles.length
|
||||
const totalTokens = rwTokens + roTokens
|
||||
const rwChars = rwFiles.reduce((sum, f) => sum + f.code.length, 0)
|
||||
const roChars = roFiles.reduce((sum, f) => sum + f.code.length, 0)
|
||||
|
||||
return {
|
||||
rwFiles,
|
||||
roFiles,
|
||||
metrics: { rwTokens, roTokens, totalFiles, totalTokens, rwChars, roChars },
|
||||
}
|
||||
}, [originalCode, dependencyCode])
|
||||
|
||||
const toggleSection = useCallback((section: string): void => {
|
||||
setExpandedSections(prev => {
|
||||
const next = new Set(prev)
|
||||
if (next.has(section)) {
|
||||
next.delete(section)
|
||||
} else {
|
||||
next.add(section)
|
||||
}
|
||||
return next
|
||||
})
|
||||
}, [])
|
||||
|
||||
const toggleFile = useCallback((fileKey: string): void => {
|
||||
setExpandedFiles(prev => {
|
||||
const next = new Set(prev)
|
||||
if (next.has(fileKey)) {
|
||||
next.delete(fileKey)
|
||||
} else {
|
||||
next.add(fileKey)
|
||||
}
|
||||
return next
|
||||
})
|
||||
}, [])
|
||||
|
||||
if (!originalCode && !dependencyCode) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="bg-white dark:bg-zinc-900 rounded-sm border border-zinc-200 dark:border-zinc-800">
|
||||
<div
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={() => setIsExpanded(!isExpanded)}
|
||||
onKeyDown={e => { if (e.key === "Enter" || e.key === " ") setIsExpanded(!isExpanded) }}
|
||||
className="w-full p-6 flex items-center justify-between hover:bg-zinc-50 dark:hover:bg-zinc-800 transition-colors rounded-sm cursor-pointer"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<Code className="h-4 w-4 text-zinc-400" />
|
||||
<h2 className="text-xl font-bold text-zinc-900 dark:text-white">Code Context</h2>
|
||||
<InfoIcon
|
||||
content="The code provided to the LLM for optimization. Read-writable code can be modified, read-only dependencies are for reference only."
|
||||
side="right"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="flex items-center gap-4 text-sm text-zinc-500 dark:text-zinc-400">
|
||||
<span className="flex items-center gap-1">
|
||||
<Hash className="h-4 w-4" />
|
||||
{metrics.totalTokens.toLocaleString()} tokens
|
||||
</span>
|
||||
<span className="flex items-center gap-1">
|
||||
<FolderTree className="h-4 w-4" />
|
||||
{metrics.totalFiles} files
|
||||
</span>
|
||||
</div>
|
||||
<ChevronDown className={`h-4 w-4 text-zinc-400 transition-transform duration-200 ${isExpanded ? '' : '-rotate-90'}`} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isExpanded && (
|
||||
<div className="px-6 pb-6 pt-0 border-t border-zinc-200 dark:border-zinc-800 space-y-4">
|
||||
{(functionName || filePath) && (
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 py-4">
|
||||
{functionName && (
|
||||
<div className="flex flex-col">
|
||||
<span className="text-xs text-zinc-500 dark:text-zinc-400 mb-1">Function</span>
|
||||
<code className="text-sm font-mono text-zinc-900 dark:text-white bg-zinc-100 dark:bg-zinc-800 px-2 py-1 rounded-sm w-fit">
|
||||
{functionName}
|
||||
</code>
|
||||
</div>
|
||||
)}
|
||||
{filePath && (
|
||||
<div className="flex flex-col">
|
||||
<span className="text-xs text-zinc-500 dark:text-zinc-400 mb-1">File</span>
|
||||
<code className="text-sm font-mono text-zinc-900 dark:text-white bg-zinc-100 dark:bg-zinc-800 px-2 py-1 rounded-sm w-fit truncate max-w-full" title={filePath}>
|
||||
{filePath}
|
||||
</code>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{rwFiles.length > 0 && roFiles.length > 0 && (
|
||||
<div className="p-4 bg-zinc-50 dark:bg-zinc-950 rounded-sm border border-zinc-200 dark:border-zinc-800">
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<Hash className="h-4 w-4 text-zinc-400" />
|
||||
<span className="text-sm font-medium text-zinc-700 dark:text-zinc-300">
|
||||
Token Distribution (estimated)
|
||||
</span>
|
||||
</div>
|
||||
<TokenDistributionBar
|
||||
rwTokens={metrics.rwTokens}
|
||||
roTokens={metrics.roTokens}
|
||||
totalTokens={metrics.totalTokens || 1}
|
||||
/>
|
||||
<div className="flex gap-4 text-xs">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<div className="w-3 h-3 rounded-sm bg-emerald-500" />
|
||||
<span className="text-zinc-600 dark:text-zinc-400">
|
||||
Read-Writable: {metrics.rwTokens.toLocaleString()} ({rwFiles.length} files)
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<div className="w-3 h-3 rounded-sm bg-slate-500" />
|
||||
<span className="text-zinc-600 dark:text-zinc-400">
|
||||
Read-Only: {metrics.roTokens.toLocaleString()} ({roFiles.length} files)
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{rwFiles.length > 0 && (
|
||||
<CodeGroupSection
|
||||
title="Read-Writable Code"
|
||||
subtitle="Function to optimize + first-degree helpers"
|
||||
accentColor="emerald"
|
||||
tokenCount={metrics.rwTokens}
|
||||
charCount={metrics.rwChars}
|
||||
files={rwFiles}
|
||||
isExpanded={expandedSections.has("rw")}
|
||||
onToggle={() => toggleSection("rw")}
|
||||
expandedFiles={expandedFiles}
|
||||
onToggleFile={toggleFile}
|
||||
sectionKey="rw"
|
||||
/>
|
||||
)}
|
||||
|
||||
{roFiles.length > 0 && (
|
||||
<CodeGroupSection
|
||||
title="Read-Only Dependencies"
|
||||
subtitle="Transitive dependencies for context"
|
||||
accentColor="slate"
|
||||
tokenCount={metrics.roTokens}
|
||||
charCount={metrics.roChars}
|
||||
files={roFiles}
|
||||
isExpanded={expandedSections.has("ro")}
|
||||
onToggle={() => toggleSection("ro")}
|
||||
expandedFiles={expandedFiles}
|
||||
onToggleFile={toggleFile}
|
||||
sectionKey="ro"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
})
|
||||
|
||||
interface CodeGroupSectionProps {
|
||||
title: string
|
||||
subtitle: string
|
||||
accentColor: "emerald" | "slate"
|
||||
tokenCount: number
|
||||
charCount: number
|
||||
files: ParsedFile[]
|
||||
isExpanded: boolean
|
||||
onToggle: () => void
|
||||
expandedFiles: Set<string>
|
||||
onToggleFile: (fileKey: string) => void
|
||||
sectionKey: string
|
||||
}
|
||||
|
||||
function getAccentColorClasses(accentColor: "emerald" | "slate"): { border: string; bg: string; icon: string } {
|
||||
switch (accentColor) {
|
||||
case "emerald":
|
||||
return {
|
||||
border: "border-zinc-200 dark:border-zinc-800",
|
||||
bg: "bg-zinc-50 dark:bg-zinc-900",
|
||||
icon: "text-emerald-500",
|
||||
}
|
||||
case "slate":
|
||||
return {
|
||||
border: "border-zinc-200 dark:border-zinc-800",
|
||||
bg: "bg-zinc-50 dark:bg-zinc-900",
|
||||
icon: "text-zinc-500",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const CodeGroupSection = memo(function CodeGroupSection({
|
||||
title,
|
||||
subtitle,
|
||||
accentColor,
|
||||
tokenCount,
|
||||
charCount,
|
||||
files,
|
||||
isExpanded,
|
||||
onToggle,
|
||||
expandedFiles,
|
||||
onToggleFile,
|
||||
sectionKey,
|
||||
}: CodeGroupSectionProps) {
|
||||
const { border: borderColor, bg: bgColor, icon: iconColor } = getAccentColorClasses(accentColor)
|
||||
|
||||
return (
|
||||
<div className={`rounded-sm border ${borderColor} overflow-hidden`}>
|
||||
<div
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={onToggle}
|
||||
onKeyDown={e => { if (e.key === "Enter" || e.key === " ") onToggle() }}
|
||||
className={`w-full p-4 flex items-center justify-between hover:bg-zinc-100 dark:hover:bg-zinc-800 transition-colors cursor-pointer ${bgColor}`}
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<FileText className={`h-4 w-4 ${iconColor}`} />
|
||||
<div className="text-left">
|
||||
<span className="text-sm font-medium text-zinc-900 dark:text-white">{title}</span>
|
||||
<span className="text-xs text-zinc-500 dark:text-zinc-400 ml-2">{subtitle}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="text-xs text-zinc-500 dark:text-zinc-400">
|
||||
{tokenCount.toLocaleString()} tokens · {charCount.toLocaleString()} chars · {files.length} files
|
||||
</span>
|
||||
<ChevronDown className={`h-4 w-4 text-zinc-400 transition-transform duration-200 ${isExpanded ? '' : '-rotate-90'}`} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isExpanded && (
|
||||
<div className="border-t border-zinc-200 dark:border-zinc-800 divide-y divide-zinc-200 dark:divide-zinc-800">
|
||||
{files.map((file, index) => {
|
||||
const fileKey = `${sectionKey}-${index}`
|
||||
const isFileExpanded = expandedFiles.has(fileKey)
|
||||
return (
|
||||
<div key={fileKey}>
|
||||
<div className="px-4 py-2 flex items-center justify-between bg-zinc-100 dark:bg-zinc-950">
|
||||
<div
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={() => onToggleFile(fileKey)}
|
||||
onKeyDown={e => { if (e.key === "Enter" || e.key === " ") onToggleFile(fileKey) }}
|
||||
className="flex items-center gap-2 cursor-pointer hover:opacity-80 flex-1"
|
||||
>
|
||||
<FileText className="h-3.5 w-3.5 text-zinc-400" />
|
||||
<span className="text-sm font-mono font-medium text-zinc-900 dark:text-white">
|
||||
{file.filename}
|
||||
</span>
|
||||
<span className="text-xs text-zinc-500 dark:text-zinc-400" title={file.path}>
|
||||
{file.path !== file.filename && `(${file.path})`}
|
||||
</span>
|
||||
<ChevronDown className={`h-3.5 w-3.5 text-zinc-400 transition-transform duration-200 ${isFileExpanded ? '' : '-rotate-90'}`} />
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xs text-zinc-500 dark:text-zinc-400">
|
||||
{file.code.split("\n").length} lines
|
||||
</span>
|
||||
<CopyButton text={file.code} label={file.filename} size="sm" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isFileExpanded && (
|
||||
<div className="max-h-96 overflow-y-auto">
|
||||
<CodeHighlighter
|
||||
language={file.language}
|
||||
code={file.code}
|
||||
customStyle={CODE_STYLE}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
})
|
||||
|
||||
interface TokenDistributionBarProps {
|
||||
rwTokens: number
|
||||
roTokens: number
|
||||
totalTokens: number
|
||||
}
|
||||
|
||||
const TokenDistributionBar = memo(function TokenDistributionBar({
|
||||
rwTokens,
|
||||
roTokens,
|
||||
totalTokens,
|
||||
}: TokenDistributionBarProps) {
|
||||
const rwPercent = Math.round((rwTokens / totalTokens) * 100)
|
||||
const roPercent = Math.round((roTokens / totalTokens) * 100)
|
||||
|
||||
return (
|
||||
<div className="flex h-6 rounded-sm overflow-hidden border border-zinc-200 dark:border-zinc-800 mb-2">
|
||||
<div
|
||||
className="bg-emerald-500 flex items-center justify-center text-white text-xs font-semibold"
|
||||
style={{ width: `${rwPercent}%` }}
|
||||
>
|
||||
{rwPercent > 15 && `${rwPercent}%`}
|
||||
</div>
|
||||
<div
|
||||
className="bg-slate-500 flex items-center justify-center text-white text-xs font-semibold"
|
||||
style={{ width: `${roPercent}%` }}
|
||||
>
|
||||
{roPercent > 15 && `${roPercent}%`}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
|
|
@ -1,172 +0,0 @@
|
|||
"use client"
|
||||
|
||||
import dynamic from "next/dynamic"
|
||||
import { memo } from "react"
|
||||
|
||||
const SyntaxHighlighter = dynamic(
|
||||
() => import("react-syntax-highlighter").then(m => m.Prism),
|
||||
{
|
||||
ssr: false,
|
||||
loading: () => (
|
||||
<div className="animate-pulse bg-zinc-800 rounded p-4 min-h-[100px]">
|
||||
<div className="h-4 bg-zinc-700 rounded w-3/4 mb-2" />
|
||||
<div className="h-4 bg-zinc-700 rounded w-1/2 mb-2" />
|
||||
<div className="h-4 bg-zinc-700 rounded w-2/3" />
|
||||
</div>
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
export const zincDarkTheme = {
|
||||
'code[class*="language-"]': {
|
||||
color: 'rgb(250, 250, 250)',
|
||||
background: 'none',
|
||||
fontFamily: 'var(--font-mono)',
|
||||
fontSize: '1em',
|
||||
textAlign: 'left',
|
||||
whiteSpace: 'pre',
|
||||
wordSpacing: 'normal',
|
||||
wordBreak: 'normal',
|
||||
wordWrap: 'normal',
|
||||
lineHeight: '1.5',
|
||||
tabSize: 4,
|
||||
hyphens: 'none',
|
||||
},
|
||||
'pre[class*="language-"]': {
|
||||
color: 'rgb(250, 250, 250)',
|
||||
background: 'rgb(24, 24, 27)',
|
||||
fontFamily: 'var(--font-mono)',
|
||||
fontSize: '1em',
|
||||
textAlign: 'left',
|
||||
whiteSpace: 'pre',
|
||||
wordSpacing: 'normal',
|
||||
wordBreak: 'normal',
|
||||
wordWrap: 'normal',
|
||||
lineHeight: '1.5',
|
||||
tabSize: 4,
|
||||
hyphens: 'none',
|
||||
padding: '1em',
|
||||
margin: '0',
|
||||
overflow: 'auto',
|
||||
},
|
||||
comment: {
|
||||
color: 'rgb(113, 113, 122)',
|
||||
fontStyle: 'italic',
|
||||
},
|
||||
prolog: { color: 'rgb(113, 113, 122)' },
|
||||
doctype: { color: 'rgb(113, 113, 122)' },
|
||||
cdata: { color: 'rgb(113, 113, 122)' },
|
||||
keyword: { color: 'rgb(96, 165, 250)' },
|
||||
'control-flow': { color: 'rgb(96, 165, 250)' },
|
||||
string: { color: 'rgb(134, 239, 172)' },
|
||||
'attr-value': { color: 'rgb(134, 239, 172)' },
|
||||
function: { color: 'rgb(253, 224, 71)' },
|
||||
'class-name': { color: 'rgb(253, 224, 71)' },
|
||||
number: { color: 'rgb(251, 146, 60)' },
|
||||
boolean: { color: 'rgb(251, 146, 60)' },
|
||||
operator: { color: 'rgb(161, 161, 170)' },
|
||||
punctuation: { color: 'rgb(161, 161, 170)' },
|
||||
variable: { color: 'rgb(250, 250, 250)' },
|
||||
property: { color: 'rgb(250, 250, 250)' },
|
||||
tag: { color: 'rgb(96, 165, 250)' },
|
||||
'attr-name': { color: 'rgb(250, 250, 250)' },
|
||||
namespace: { opacity: 0.7 },
|
||||
selector: { color: 'rgb(253, 224, 71)' },
|
||||
important: {
|
||||
color: 'rgb(251, 146, 60)',
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
atrule: { color: 'rgb(96, 165, 250)' },
|
||||
builtin: { color: 'rgb(253, 224, 71)' },
|
||||
entity: {
|
||||
color: 'rgb(250, 250, 250)',
|
||||
cursor: 'help',
|
||||
},
|
||||
url: {
|
||||
color: 'rgb(96, 165, 250)',
|
||||
textDecoration: 'underline',
|
||||
},
|
||||
inserted: {
|
||||
color: 'rgb(134, 239, 172)',
|
||||
background: 'rgba(134, 239, 172, 0.1)',
|
||||
},
|
||||
deleted: {
|
||||
color: 'rgb(248, 113, 113)',
|
||||
background: 'rgba(248, 113, 113, 0.1)',
|
||||
},
|
||||
} as const
|
||||
|
||||
export const CODE_STYLE = {
|
||||
margin: 0,
|
||||
padding: "1rem",
|
||||
fontSize: "0.875rem",
|
||||
lineHeight: 1.5,
|
||||
background: 'rgb(24, 24, 27)',
|
||||
} as const
|
||||
|
||||
export const CODE_STYLE_RELAXED = {
|
||||
margin: 0,
|
||||
padding: "1rem",
|
||||
fontSize: "0.875rem",
|
||||
lineHeight: 1.6,
|
||||
background: 'rgb(24, 24, 27)',
|
||||
} as const
|
||||
|
||||
export const CODE_STYLE_SMALL = {
|
||||
margin: 0,
|
||||
padding: "1rem",
|
||||
fontSize: "0.8125rem",
|
||||
lineHeight: 1.5,
|
||||
background: 'rgb(24, 24, 27)',
|
||||
} as const
|
||||
|
||||
interface CodeHighlighterProps {
|
||||
code: string
|
||||
language: string
|
||||
showLineNumbers?: boolean
|
||||
customStyle?: React.CSSProperties
|
||||
highlightLines?: number[]
|
||||
}
|
||||
|
||||
const highlightStyle = {
|
||||
backgroundColor: 'rgba(250, 204, 21, 0.15)',
|
||||
display: 'block',
|
||||
marginLeft: '-1rem',
|
||||
marginRight: '-1rem',
|
||||
paddingLeft: '1rem',
|
||||
paddingRight: '1rem',
|
||||
borderLeft: '3px solid rgb(250, 204, 21)',
|
||||
}
|
||||
|
||||
export const CodeHighlighter = memo(function CodeHighlighter({
|
||||
code,
|
||||
language,
|
||||
showLineNumbers = true,
|
||||
customStyle = CODE_STYLE,
|
||||
highlightLines,
|
||||
}: CodeHighlighterProps) {
|
||||
const lineProps = highlightLines && highlightLines.length > 0
|
||||
? (lineNumber: number) => {
|
||||
const isHighlighted = highlightLines.includes(lineNumber)
|
||||
return {
|
||||
style: isHighlighted ? highlightStyle : { display: 'block' },
|
||||
'data-highlighted': isHighlighted ? 'true' : undefined,
|
||||
}
|
||||
}
|
||||
: undefined
|
||||
|
||||
const shouldWrapLines = !!(highlightLines && highlightLines.length > 0)
|
||||
|
||||
return (
|
||||
<SyntaxHighlighter
|
||||
language={language}
|
||||
style={zincDarkTheme}
|
||||
customStyle={customStyle}
|
||||
showLineNumbers={showLineNumbers}
|
||||
wrapLines={shouldWrapLines}
|
||||
lineProps={shouldWrapLines ? lineProps : undefined}
|
||||
>
|
||||
{code}
|
||||
</SyntaxHighlighter>
|
||||
)
|
||||
})
|
||||
27
js/cf-webapp/src/components/observability/column-header.tsx
Normal file
27
js/cf-webapp/src/components/observability/column-header.tsx
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
"use client"
|
||||
|
||||
import { InfoIcon } from "./info-icon"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
interface ColumnHeaderProps {
|
||||
label: string
|
||||
tooltip: string
|
||||
className?: string
|
||||
}
|
||||
|
||||
export function ColumnHeader({ label, tooltip, className }: ColumnHeaderProps) {
|
||||
return (
|
||||
<th
|
||||
className={cn(
|
||||
"px-6 py-3 text-left text-xs font-medium text-gray-700 dark:text-gray-300",
|
||||
"uppercase tracking-wider",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span>{label}</span>
|
||||
<InfoIcon content={tooltip} side="top" />
|
||||
</div>
|
||||
</th>
|
||||
)
|
||||
}
|
||||
|
|
@ -1,201 +0,0 @@
|
|||
"use client"
|
||||
|
||||
import { useState, useCallback, memo } from "react"
|
||||
import {
|
||||
XCircle,
|
||||
AlertCircle,
|
||||
AlertTriangle,
|
||||
ChevronDown,
|
||||
} from "lucide-react"
|
||||
import { CopyButton } from "./copy-button"
|
||||
|
||||
interface ErrorContext {
|
||||
test_name?: string
|
||||
failure_reason?: string
|
||||
test_output?: string
|
||||
expected?: string
|
||||
actual?: string
|
||||
}
|
||||
|
||||
interface TraceError {
|
||||
id: string
|
||||
error_type: string
|
||||
severity: string
|
||||
error_message: string
|
||||
context: ErrorContext | null
|
||||
created_at: Date
|
||||
}
|
||||
|
||||
interface ErrorsSectionProps {
|
||||
errors: TraceError[]
|
||||
}
|
||||
|
||||
export const ErrorsSection = memo(function ErrorsSection({ errors }: ErrorsSectionProps) {
|
||||
const [expandedErrors, setExpandedErrors] = useState<Set<string>>(new Set())
|
||||
|
||||
const toggleError = useCallback((errorId: string) => {
|
||||
setExpandedErrors(prev => {
|
||||
const next = new Set(prev)
|
||||
if (next.has(errorId)) {
|
||||
next.delete(errorId)
|
||||
} else {
|
||||
next.add(errorId)
|
||||
}
|
||||
return next
|
||||
})
|
||||
}, [])
|
||||
|
||||
if (errors.length === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="bg-white dark:bg-zinc-900 rounded-sm border border-zinc-200 dark:border-zinc-700">
|
||||
<div className="p-6 border-b border-zinc-200 dark:border-zinc-700">
|
||||
<div className="flex items-center gap-3">
|
||||
<AlertCircle className="h-5 w-5 text-red-600 dark:text-red-400" />
|
||||
<h2 className="text-xl font-bold text-red-600 dark:text-red-400">Errors</h2>
|
||||
<span className="text-red-400 border border-red-600 text-sm px-2.5 py-0.5 rounded-sm font-semibold">
|
||||
{errors.length}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="divide-y divide-zinc-200 dark:divide-zinc-700">
|
||||
{errors.map(error => {
|
||||
const isExpanded = expandedErrors.has(error.id)
|
||||
const isTestFailure = error.error_type === "test_failure"
|
||||
const hasContext = error.context && Object.keys(error.context).length > 0
|
||||
|
||||
const SeverityIcon =
|
||||
error.severity === "critical" || error.severity === "error" ? XCircle : AlertTriangle
|
||||
|
||||
let severityColor: string
|
||||
if (error.severity === "critical") {
|
||||
severityColor = "text-red-400 border border-red-600 px-1.5 py-0.5"
|
||||
} else if (error.severity === "error") {
|
||||
severityColor = "text-orange-400 border border-orange-600 px-1.5 py-0.5"
|
||||
} else {
|
||||
severityColor = "text-yellow-400 border border-yellow-600 px-1.5 py-0.5"
|
||||
}
|
||||
|
||||
return (
|
||||
<div key={error.id} className="border-l-4 border-red-500">
|
||||
<div className="w-full p-4 flex items-start gap-3">
|
||||
<div
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={() => toggleError(error.id)}
|
||||
onKeyDown={e => { if (e.key === "Enter" || e.key === " ") toggleError(error.id) }}
|
||||
className="flex items-start gap-3 cursor-pointer hover:opacity-80 flex-1 transition-opacity duration-150"
|
||||
>
|
||||
<SeverityIcon className="h-5 w-5 text-red-600 dark:text-red-400 mt-0.5 flex-shrink-0" />
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2 mb-1 flex-wrap">
|
||||
<span className="font-semibold text-zinc-900 dark:text-white">
|
||||
{error.error_type}
|
||||
</span>
|
||||
<span className={`text-xs font-semibold rounded-sm ${severityColor}`}>
|
||||
{error.severity}
|
||||
</span>
|
||||
<span className="text-xs text-zinc-500 dark:text-zinc-400 ml-auto">
|
||||
{new Date(error.created_at).toLocaleString()}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-sm text-zinc-700 dark:text-zinc-300 line-clamp-2">
|
||||
{error.error_message}
|
||||
</p>
|
||||
</div>
|
||||
{(hasContext || isTestFailure) && (
|
||||
<ChevronDown className={`h-4 w-4 text-zinc-400 transition-transform duration-200 ${isExpanded ? '' : '-rotate-90'}`} />
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<CopyButton text={error.error_message} label="error message" size="sm" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isExpanded && isTestFailure && error.context && (
|
||||
<div className="px-4 pb-4 ml-8">
|
||||
<div className="p-4 bg-zinc-50 dark:bg-zinc-800 rounded-sm border border-zinc-200 dark:border-zinc-700">
|
||||
<h4 className="text-sm font-semibold text-red-600 dark:text-red-400 mb-3">
|
||||
Test Failure Details
|
||||
</h4>
|
||||
|
||||
{error.context.test_name && (
|
||||
<div className="mb-3">
|
||||
<span className="text-xs font-medium text-zinc-600 dark:text-zinc-400 uppercase tracking-wide">
|
||||
Test Name
|
||||
</span>
|
||||
<p className="text-sm text-zinc-900 dark:text-white font-mono mt-1">
|
||||
{error.context.test_name}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{error.context.failure_reason && (
|
||||
<div className="mb-3">
|
||||
<span className="text-xs font-medium text-zinc-600 dark:text-zinc-400 uppercase tracking-wide">
|
||||
Failure Reason
|
||||
</span>
|
||||
<p className="text-sm text-zinc-900 dark:text-white mt-1 whitespace-pre-wrap">
|
||||
{error.context.failure_reason}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{error.context.expected && (
|
||||
<div className="mb-3">
|
||||
<span className="text-xs font-medium text-zinc-600 dark:text-zinc-400 uppercase tracking-wide">
|
||||
Expected
|
||||
</span>
|
||||
<pre className="text-xs text-zinc-900 dark:text-white mt-1 bg-white dark:bg-zinc-900 p-2 rounded-sm overflow-x-auto font-mono">
|
||||
{String(error.context.expected)}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{error.context.actual && (
|
||||
<div className="mb-3">
|
||||
<span className="text-xs font-medium text-zinc-600 dark:text-zinc-400 uppercase tracking-wide">
|
||||
Actual
|
||||
</span>
|
||||
<pre className="text-xs text-zinc-900 dark:text-white mt-1 bg-white dark:bg-zinc-900 p-2 rounded-sm overflow-x-auto font-mono">
|
||||
{String(error.context.actual)}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{error.context.test_output && (
|
||||
<div>
|
||||
<span className="text-xs font-medium text-zinc-600 dark:text-zinc-400 uppercase tracking-wide">
|
||||
Test Output
|
||||
</span>
|
||||
<pre className="text-xs text-zinc-900 dark:text-white mt-1 bg-white dark:bg-zinc-900 p-2 rounded-sm overflow-x-auto whitespace-pre-wrap font-mono max-h-48">
|
||||
{String(error.context.test_output)}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isExpanded && !isTestFailure && hasContext && (
|
||||
<div className="px-4 pb-4 ml-8">
|
||||
<div className="p-4 bg-zinc-50 dark:bg-zinc-900 rounded-sm border border-zinc-200 dark:border-zinc-700">
|
||||
<span className="text-xs font-medium text-zinc-600 dark:text-zinc-400 uppercase tracking-wide">
|
||||
Context
|
||||
</span>
|
||||
<pre className="text-xs text-zinc-900 dark:text-white mt-2 overflow-auto whitespace-pre-wrap font-mono max-h-48">
|
||||
{JSON.stringify(error.context, null, 2)}
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
|
|
@ -1,204 +0,0 @@
|
|||
"use client"
|
||||
|
||||
import { memo, useMemo, useState, useRef, useEffect } from "react"
|
||||
import { Code, FileText, ChevronDown } from "lucide-react"
|
||||
import { CodeHighlighter, CODE_STYLE_RELAXED } from "./code-highlighter"
|
||||
import { CopyButton } from "./copy-button"
|
||||
import { findFunctionInCode, type FunctionLocation } from "./python-parser"
|
||||
|
||||
interface FunctionToOptimizeSectionProps {
|
||||
functionName: string | null
|
||||
filePath: string | null
|
||||
originalCode: string | null
|
||||
}
|
||||
|
||||
interface ParsedFile {
|
||||
path: string
|
||||
filename: string
|
||||
language: string
|
||||
code: string
|
||||
}
|
||||
|
||||
function getFilename(path: string): string {
|
||||
return path.split("/").pop() || path
|
||||
}
|
||||
|
||||
function parseMarkdownCodeBlocks(markdown: string): ParsedFile[] {
|
||||
const files: ParsedFile[] = []
|
||||
const regex = /```(\w+):([^\n]+)\n([\s\S]*?)```/g
|
||||
let match
|
||||
|
||||
while ((match = regex.exec(markdown)) !== null) {
|
||||
const [, language, path, code] = match
|
||||
files.push({
|
||||
path,
|
||||
filename: getFilename(path),
|
||||
language: language || "python",
|
||||
code: code.trimEnd(),
|
||||
})
|
||||
}
|
||||
|
||||
return files
|
||||
}
|
||||
|
||||
export const FunctionToOptimizeSection = memo(function FunctionToOptimizeSection({
|
||||
functionName,
|
||||
filePath,
|
||||
originalCode,
|
||||
}: FunctionToOptimizeSectionProps) {
|
||||
const [isExpanded, setIsExpanded] = useState(true)
|
||||
const [functionLocation, setFunctionLocation] = useState<FunctionLocation | null>(null)
|
||||
const [actualFile, setActualFile] = useState<ParsedFile | null>(null)
|
||||
const codeContainerRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
const allFiles = useMemo(() => {
|
||||
if (!originalCode) return []
|
||||
return parseMarkdownCodeBlocks(originalCode)
|
||||
}, [originalCode])
|
||||
|
||||
useEffect(() => {
|
||||
if (!functionName || allFiles.length === 0) {
|
||||
setFunctionLocation(null)
|
||||
setActualFile(null)
|
||||
return
|
||||
}
|
||||
|
||||
let cancelled = false
|
||||
|
||||
async function findFunction() {
|
||||
const searchPromises = allFiles.map(async (file) => {
|
||||
const location = await findFunctionInCode(file.code, functionName!)
|
||||
return location ? { file, location } : null
|
||||
})
|
||||
|
||||
const results = await Promise.all(searchPromises)
|
||||
if (cancelled) return
|
||||
|
||||
const found = results.find(r => r !== null)
|
||||
if (found) {
|
||||
setFunctionLocation(found.location)
|
||||
setActualFile(found.file)
|
||||
return
|
||||
}
|
||||
|
||||
let fallbackFile = allFiles[0]
|
||||
if (filePath) {
|
||||
const match = allFiles.find(f =>
|
||||
filePath.endsWith(f.path) || f.path.endsWith(filePath) || f.path === filePath
|
||||
)
|
||||
if (match) fallbackFile = match
|
||||
}
|
||||
setFunctionLocation(null)
|
||||
setActualFile(fallbackFile)
|
||||
}
|
||||
|
||||
findFunction()
|
||||
return () => { cancelled = true }
|
||||
}, [functionName, filePath, allFiles])
|
||||
|
||||
const functionFile = actualFile ?? allFiles[0] ?? null
|
||||
|
||||
const functionLines = useMemo(() => {
|
||||
if (!functionLocation) return null
|
||||
const lines: number[] = []
|
||||
for (let i = functionLocation.startLine; i <= functionLocation.endLine; i++) {
|
||||
lines.push(i)
|
||||
}
|
||||
return lines
|
||||
}, [functionLocation])
|
||||
|
||||
useEffect(() => {
|
||||
if (!isExpanded || !functionLocation || !codeContainerRef.current) return
|
||||
|
||||
const scrollToFunction = () => {
|
||||
if (!codeContainerRef.current) return
|
||||
const container = codeContainerRef.current
|
||||
|
||||
const lineHeight = 22.4
|
||||
const paddingTop = 16
|
||||
const targetLine = functionLocation.startLine - 1
|
||||
const scrollPosition = paddingTop + (targetLine * lineHeight) - (container.clientHeight / 3)
|
||||
|
||||
container.scrollTo({
|
||||
top: Math.max(0, scrollPosition),
|
||||
behavior: 'smooth'
|
||||
})
|
||||
}
|
||||
|
||||
const timer = setTimeout(scrollToFunction, 300)
|
||||
return () => clearTimeout(timer)
|
||||
}, [isExpanded, functionLocation])
|
||||
|
||||
if (!functionFile) {
|
||||
return null
|
||||
}
|
||||
|
||||
const highlightLines = functionLines && functionLines.length > 0 ? functionLines : undefined
|
||||
|
||||
return (
|
||||
<div className="bg-white dark:bg-zinc-900 rounded-sm border border-zinc-200 dark:border-zinc-800 overflow-hidden">
|
||||
<div className="w-full p-4 bg-zinc-950 flex items-center justify-between">
|
||||
<div
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={() => setIsExpanded(!isExpanded)}
|
||||
onKeyDown={e => { if (e.key === "Enter" || e.key === " ") setIsExpanded(!isExpanded) }}
|
||||
className="flex items-center gap-3 cursor-pointer hover:opacity-80 flex-1"
|
||||
>
|
||||
<div className="p-2 bg-zinc-800 rounded-sm">
|
||||
<Code className="h-4 w-4 text-zinc-400" />
|
||||
</div>
|
||||
<div className="text-left">
|
||||
<h2 className="text-lg font-semibold text-zinc-50">
|
||||
Function to Optimize
|
||||
</h2>
|
||||
<div className="flex items-center gap-2 mt-1">
|
||||
{functionName && (
|
||||
<code className="text-sm font-mono text-zinc-300 bg-zinc-800 px-2 py-0.5 rounded-sm">
|
||||
{functionName}
|
||||
</code>
|
||||
)}
|
||||
{functionLocation && (
|
||||
<span className="text-xs text-zinc-500">
|
||||
lines {functionLocation.startLine}-{functionLocation.endLine}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<ChevronDown className={`h-4 w-4 text-zinc-500 transition-transform duration-200 ${isExpanded ? '' : '-rotate-90'}`} />
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<CopyButton text={functionFile.code} label="function code" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isExpanded && (
|
||||
<>
|
||||
<div className="px-4 py-2 bg-zinc-900 border-t border-b border-zinc-800 flex items-center gap-2">
|
||||
<FileText className="h-4 w-4 text-zinc-500" />
|
||||
<span className="text-sm font-mono font-medium text-zinc-300">
|
||||
{functionFile.filename}
|
||||
</span>
|
||||
{functionFile.path !== functionFile.filename && (
|
||||
<span className="text-xs font-mono text-zinc-500">
|
||||
({functionFile.path})
|
||||
</span>
|
||||
)}
|
||||
<span className="text-xs text-zinc-500 ml-auto">
|
||||
{functionFile.code.split("\n").length} lines
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div ref={codeContainerRef} className="max-h-[500px] overflow-y-auto">
|
||||
<CodeHighlighter
|
||||
language={functionFile.language}
|
||||
code={functionFile.code}
|
||||
customStyle={CODE_STYLE_RELAXED}
|
||||
highlightLines={highlightLines}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
})
|
||||
51
js/cf-webapp/src/components/observability/help-button.tsx
Normal file
51
js/cf-webapp/src/components/observability/help-button.tsx
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
"use client"
|
||||
|
||||
import { Info } from "lucide-react"
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
interface HelpButtonProps {
|
||||
title: string
|
||||
content: React.ReactNode
|
||||
size?: "sm" | "md"
|
||||
triggerClassName?: string
|
||||
}
|
||||
|
||||
export function HelpButton({ title, content, size = "sm", triggerClassName }: HelpButtonProps) {
|
||||
return (
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
className={cn(
|
||||
"inline-flex items-center justify-center gap-2",
|
||||
"text-gray-600 hover:text-blue-600 dark:text-gray-400 dark:hover:text-blue-400",
|
||||
"transition-colors duration-200",
|
||||
"focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 rounded-md",
|
||||
size === "sm" ? "p-2" : "px-3 py-2",
|
||||
triggerClassName,
|
||||
)}
|
||||
aria-label={`Open help: ${title}`}
|
||||
>
|
||||
<Info className={size === "sm" ? "h-5 w-5" : "h-4 w-4"} />
|
||||
{size === "md" && <span className="text-sm font-medium">Help</span>}
|
||||
</button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-w-2xl max-h-[80vh] overflow-y-auto">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{title}</DialogTitle>
|
||||
<DialogDescription asChild>
|
||||
<div className="text-left pt-4">{content}</div>
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
export { TraceSearch } from "./trace-search"
|
||||
export { TraceSummary } from "./trace-summary"
|
||||
export { TimelinePageView } from "./timeline-page-view"
|
||||
export { transformToTimelineSections } from "./timeline-types"
|
||||
export { ErrorsSection } from "./errors-section"
|
||||
export { CodeHighlighter, CODE_STYLE, CODE_STYLE_RELAXED, CODE_STYLE_SMALL } from "./code-highlighter"
|
||||
export { CopyButton } from "./copy-button"
|
||||
export { InfoIcon } from "./info-icon"
|
||||
export { getTraceSource } from "./utils"
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
"use client"
|
||||
|
||||
import Link from "next/link"
|
||||
import { usePathname } from "next/navigation"
|
||||
import { Activity, ListTree } from "lucide-react"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const navItems = [
|
||||
{ href: "/observability/traces", label: "Traces", icon: ListTree },
|
||||
{ href: "/observability/llm-calls", label: "LLM Calls", icon: Activity },
|
||||
]
|
||||
|
||||
export function ObservabilityNav() {
|
||||
const pathname = usePathname()
|
||||
|
||||
return (
|
||||
<nav className="bg-white dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700 sticky top-0 z-10">
|
||||
<div className="container mx-auto px-6 py-3">
|
||||
<div className="flex items-center justify-between">
|
||||
{/* Logo/Title */}
|
||||
<div className="flex items-center gap-2">
|
||||
<Activity className="h-6 w-6 text-blue-600 dark:text-blue-400" />
|
||||
<h1 className="text-xl font-bold text-gray-900 dark:text-white">
|
||||
Codeflash Observability
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
{/* Navigation Links */}
|
||||
<div className="flex items-center gap-2">
|
||||
{navItems.map(item => {
|
||||
const Icon = item.icon
|
||||
const isActive = pathname === item.href || pathname.startsWith(item.href + "/")
|
||||
|
||||
return (
|
||||
<Link
|
||||
key={item.href}
|
||||
href={item.href}
|
||||
className={cn(
|
||||
"flex items-center gap-2 px-4 py-2 rounded-lg text-sm font-medium transition-colors",
|
||||
isActive
|
||||
? "bg-blue-50 dark:bg-blue-900/20 text-blue-700 dark:text-blue-300"
|
||||
: "text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700 hover:text-gray-900 dark:hover:text-white",
|
||||
)}
|
||||
>
|
||||
<Icon className="h-4 w-4" />
|
||||
{item.label}
|
||||
</Link>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,223 @@
|
|||
"use client"
|
||||
|
||||
import {
|
||||
extractExplainTag,
|
||||
extractRankTag,
|
||||
getResponseContentForParsing,
|
||||
splitMarkdownCodeBlocks,
|
||||
type ResponseSegment,
|
||||
} from "@/lib/observability-response-parse"
|
||||
import { CopyButton } from "@/components/observability/copy-button"
|
||||
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter"
|
||||
import { oneDark } from "react-syntax-highlighter/dist/esm/styles/prism"
|
||||
import { useState } from "react"
|
||||
|
||||
interface ParsedResponseViewProps {
|
||||
rawResponse: string
|
||||
callType: string | null
|
||||
}
|
||||
|
||||
/** Try to parse JSON and return pretty-printed version, or null if not JSON */
|
||||
function tryFormatJSON(content: string): string | null {
|
||||
try {
|
||||
const parsed = JSON.parse(content)
|
||||
return JSON.stringify(parsed, null, 2)
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export function ParsedResponseView({ rawResponse, callType }: ParsedResponseViewProps) {
|
||||
const [showRaw, setShowRaw] = useState(false)
|
||||
const isRanking = callType === "ranking"
|
||||
// Use inner message content when raw_response is API JSON (e.g. OpenAI)
|
||||
const contentForParsing = getResponseContentForParsing(rawResponse)
|
||||
const rankContent = isRanking ? extractRankTag(contentForParsing) : null
|
||||
const explainContent = isRanking ? extractExplainTag(contentForParsing) : null
|
||||
const hasRankingSections = isRanking && (rankContent != null || explainContent != null)
|
||||
|
||||
const segments: ResponseSegment[] = hasRankingSections
|
||||
? []
|
||||
: splitMarkdownCodeBlocks(contentForParsing)
|
||||
const hasSegments = segments.length > 0
|
||||
|
||||
// Check if raw response is JSON (for fallback display)
|
||||
const formattedJSON = tryFormatJSON(rawResponse)
|
||||
const isJSON = formattedJSON != null
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{/* View raw toggle button - always visible in header */}
|
||||
<div className="flex items-center justify-end gap-2 pb-2 border-b border-gray-200 dark:border-gray-700">
|
||||
<button
|
||||
onClick={() => setShowRaw(!showRaw)}
|
||||
className="text-sm font-medium text-gray-600 dark:text-gray-400 hover:text-blue-600 dark:hover:text-blue-400 flex items-center gap-1.5"
|
||||
>
|
||||
<span className={showRaw ? "rotate-90 transition-transform" : "transition-transform"}>
|
||||
▶
|
||||
</span>
|
||||
{showRaw ? "Hide raw" : "View raw"}
|
||||
</button>
|
||||
<CopyButton text={rawResponse} label="raw response" size="sm" />
|
||||
</div>
|
||||
|
||||
{showRaw && (
|
||||
<div className="rounded-lg border border-gray-200 dark:border-gray-700 overflow-hidden">
|
||||
<pre className="bg-gray-50 dark:bg-gray-900 p-4 text-sm overflow-auto whitespace-pre-wrap text-gray-900 dark:text-gray-100 font-mono max-h-96">
|
||||
{rawResponse}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!showRaw && (
|
||||
<div className="space-y-4">
|
||||
{hasRankingSections && (
|
||||
<div className="space-y-4">
|
||||
{rankContent != null && (
|
||||
<div className="rounded-lg border border-gray-200 dark:border-gray-700 overflow-hidden">
|
||||
<div className="px-4 py-2 bg-gray-100 dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700 flex items-center justify-between">
|
||||
<h3 className="text-sm font-semibold text-gray-700 dark:text-gray-300">
|
||||
Ranking (best first)
|
||||
</h3>
|
||||
<CopyButton text={rankContent} label="ranking" size="sm" />
|
||||
</div>
|
||||
<div className="p-4 bg-gray-50 dark:bg-gray-900">
|
||||
<ol className="flex flex-wrap gap-2 list-none">
|
||||
{rankContent
|
||||
.split(/[\s,]+/)
|
||||
.filter(Boolean)
|
||||
.map((id, i) => {
|
||||
const pos = i + 1
|
||||
const label =
|
||||
pos === 1
|
||||
? "1st"
|
||||
: pos === 2
|
||||
? "2nd"
|
||||
: pos === 3
|
||||
? "3rd"
|
||||
: `${pos}th`
|
||||
return (
|
||||
<li
|
||||
key={`${id}-${i}`}
|
||||
className="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-md bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-600 text-sm font-mono"
|
||||
>
|
||||
<span className="text-gray-500 dark:text-gray-400 font-medium">
|
||||
{label}
|
||||
</span>
|
||||
<span className="text-gray-900 dark:text-gray-100">
|
||||
{id.trim()}
|
||||
</span>
|
||||
</li>
|
||||
)
|
||||
})}
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{explainContent != null && (
|
||||
<div className="rounded-lg border border-gray-200 dark:border-gray-700 overflow-hidden">
|
||||
<div className="px-4 py-2 bg-gray-100 dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700">
|
||||
<h3 className="text-sm font-semibold text-gray-700 dark:text-gray-300">
|
||||
Explanation
|
||||
</h3>
|
||||
</div>
|
||||
<div className="p-4 bg-gray-50 dark:bg-gray-900">
|
||||
<div className="flex items-start gap-2">
|
||||
<p className="flex-1 text-sm text-gray-700 dark:text-gray-300 whitespace-pre-wrap leading-relaxed">
|
||||
{explainContent}
|
||||
</p>
|
||||
<CopyButton text={explainContent} label="explanation" size="sm" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!hasRankingSections && hasSegments && (
|
||||
<div className="space-y-4">
|
||||
{segments.map((seg, i) => {
|
||||
const textJSON = seg.kind === "text" ? tryFormatJSON(seg.content) : null
|
||||
return seg.kind === "text" ? (
|
||||
<div key={i} className="rounded-lg border border-gray-200 dark:border-gray-700 overflow-hidden">
|
||||
{textJSON ? (
|
||||
<SyntaxHighlighter
|
||||
language="json"
|
||||
style={oneDark}
|
||||
PreTag="div"
|
||||
customStyle={{
|
||||
margin: 0,
|
||||
padding: "1rem",
|
||||
fontSize: "0.875rem",
|
||||
lineHeight: 1.5,
|
||||
}}
|
||||
codeTagProps={{ className: "text-sm" }}
|
||||
showLineNumbers={textJSON.split("\n").length > 10}
|
||||
>
|
||||
{textJSON}
|
||||
</SyntaxHighlighter>
|
||||
) : (
|
||||
<pre className="bg-gray-50 dark:bg-gray-900 p-4 text-sm overflow-auto whitespace-pre-wrap text-gray-900 dark:text-gray-100 font-mono">
|
||||
{seg.content}
|
||||
</pre>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div key={i} className="rounded-lg overflow-hidden border border-gray-200 dark:border-gray-700">
|
||||
<div className="flex items-center justify-between px-3 py-1.5 bg-gray-100 dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700">
|
||||
<span className="text-xs font-medium text-gray-600 dark:text-gray-400">
|
||||
{seg.language || "code"}
|
||||
</span>
|
||||
<CopyButton text={seg.content} label="code block" size="sm" />
|
||||
</div>
|
||||
<SyntaxHighlighter
|
||||
language={seg.language === "text" ? "plaintext" : seg.language}
|
||||
style={oneDark}
|
||||
PreTag="div"
|
||||
customStyle={{
|
||||
margin: 0,
|
||||
padding: "1rem",
|
||||
fontSize: "0.875rem",
|
||||
lineHeight: 1.5,
|
||||
}}
|
||||
codeTagProps={{ className: "text-sm" }}
|
||||
showLineNumbers={seg.content.split("\n").length > 5}
|
||||
>
|
||||
{seg.content}
|
||||
</SyntaxHighlighter>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!hasRankingSections && !hasSegments && (
|
||||
<div className="rounded-lg border border-gray-200 dark:border-gray-700 overflow-hidden">
|
||||
{isJSON ? (
|
||||
<SyntaxHighlighter
|
||||
language="json"
|
||||
style={oneDark}
|
||||
PreTag="div"
|
||||
customStyle={{
|
||||
margin: 0,
|
||||
padding: "1rem",
|
||||
fontSize: "0.875rem",
|
||||
lineHeight: 1.5,
|
||||
}}
|
||||
codeTagProps={{ className: "text-sm" }}
|
||||
showLineNumbers={formattedJSON.split("\n").length > 10}
|
||||
>
|
||||
{formattedJSON}
|
||||
</SyntaxHighlighter>
|
||||
) : (
|
||||
<pre className="bg-gray-50 dark:bg-gray-900 p-4 text-sm overflow-auto whitespace-pre-wrap text-gray-900 dark:text-gray-100 font-mono max-h-96">
|
||||
{rawResponse}
|
||||
</pre>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -1,136 +0,0 @@
|
|||
"use client"
|
||||
|
||||
import type { Node, Parser as ParserType } from "web-tree-sitter"
|
||||
|
||||
export interface FunctionLocation {
|
||||
startLine: number
|
||||
endLine: number
|
||||
}
|
||||
|
||||
let parserPromise: Promise<ParserType | null> | null = null
|
||||
|
||||
async function getParser(): Promise<ParserType | null> {
|
||||
if (typeof window === "undefined") {
|
||||
return null
|
||||
}
|
||||
|
||||
if (!parserPromise) {
|
||||
parserPromise = (async () => {
|
||||
try {
|
||||
const { Parser, Language } = await import("web-tree-sitter")
|
||||
await Parser.init({
|
||||
locateFile: (scriptName: string) => `/${scriptName}`,
|
||||
})
|
||||
const parser = new Parser()
|
||||
const Python = await Language.load("/tree-sitter-python.wasm")
|
||||
parser.setLanguage(Python)
|
||||
return parser
|
||||
} catch (error) {
|
||||
console.error("Tree-sitter initialization failed:", error)
|
||||
parserPromise = null
|
||||
throw error
|
||||
}
|
||||
})()
|
||||
}
|
||||
return parserPromise
|
||||
}
|
||||
|
||||
export async function findFunctionInCode(
|
||||
code: string,
|
||||
functionName: string
|
||||
): Promise<FunctionLocation | null> {
|
||||
try {
|
||||
const parser = await getParser()
|
||||
if (parser) {
|
||||
const tree = parser.parse(code)
|
||||
if (tree) {
|
||||
const result = findFunctionNode(tree.rootNode, functionName)
|
||||
if (result) {
|
||||
return {
|
||||
startLine: result.startPosition.row + 1,
|
||||
endLine: result.endPosition.row + 1,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn("Tree-sitter parse failed, trying regex fallback:", error)
|
||||
}
|
||||
|
||||
return findFunctionWithRegex(code, functionName)
|
||||
}
|
||||
|
||||
function findFunctionWithRegex(
|
||||
code: string,
|
||||
functionName: string
|
||||
): FunctionLocation | null {
|
||||
const lines = code.split("\n")
|
||||
|
||||
const defPattern = new RegExp(
|
||||
`^(\\s*)(async\\s+)?def\\s+${escapeRegex(functionName)}\\s*\\(`
|
||||
)
|
||||
|
||||
let startLine = -1
|
||||
let startIndent = -1
|
||||
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const line = lines[i]
|
||||
|
||||
if (startLine === -1) {
|
||||
const match = line.match(defPattern)
|
||||
if (match) {
|
||||
startLine = i + 1
|
||||
startIndent = match[1].length
|
||||
}
|
||||
} else {
|
||||
const trimmed = line.trim()
|
||||
if (trimmed === "" || trimmed.startsWith("#")) {
|
||||
continue
|
||||
}
|
||||
|
||||
const currentIndent = line.length - line.trimStart().length
|
||||
if (currentIndent <= startIndent) {
|
||||
return { startLine, endLine: i }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (startLine !== -1) {
|
||||
return { startLine, endLine: lines.length }
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
function escapeRegex(str: string): string {
|
||||
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
|
||||
}
|
||||
|
||||
function findFunctionNode(node: Node, functionName: string): Node | null {
|
||||
if (
|
||||
node.type === "function_definition" ||
|
||||
node.type === "async_function_definition"
|
||||
) {
|
||||
const nameNode = node.childForFieldName("name")
|
||||
if (nameNode && nameNode.text === functionName) {
|
||||
return node
|
||||
}
|
||||
}
|
||||
|
||||
if (node.type === "class_definition") {
|
||||
const classBody = node.childForFieldName("body")
|
||||
if (classBody) {
|
||||
for (const child of classBody.children) {
|
||||
const result = findFunctionNode(child, functionName)
|
||||
if (result) return result
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const child of node.children) {
|
||||
const result = findFunctionNode(child, functionName)
|
||||
if (result) return result
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
72
js/cf-webapp/src/components/observability/stat-card.tsx
Normal file
72
js/cf-webapp/src/components/observability/stat-card.tsx
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
"use client"
|
||||
|
||||
import {
|
||||
Database,
|
||||
Zap,
|
||||
DollarSign,
|
||||
AlertTriangle,
|
||||
Activity,
|
||||
CheckCircle2,
|
||||
Clock,
|
||||
} from "lucide-react"
|
||||
import { InfoIcon } from "./info-icon"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const variantStyles = {
|
||||
default: "",
|
||||
success: "border-l-4 border-green-500",
|
||||
warning: "border-l-4 border-yellow-500",
|
||||
error: "border-l-4 border-red-500",
|
||||
}
|
||||
|
||||
// Icon mapping - add more icons as needed
|
||||
const iconMap = {
|
||||
Database,
|
||||
Zap,
|
||||
DollarSign,
|
||||
AlertTriangle,
|
||||
Activity,
|
||||
CheckCircle2,
|
||||
Clock,
|
||||
} as const
|
||||
|
||||
type IconName = keyof typeof iconMap
|
||||
|
||||
interface StatCardProps {
|
||||
label: string
|
||||
value: string | number
|
||||
helpText?: string
|
||||
icon?: IconName
|
||||
variant?: "default" | "success" | "warning" | "error"
|
||||
className?: string
|
||||
}
|
||||
|
||||
export function StatCard({ label, value, helpText, icon, variant = "default", className }: StatCardProps) {
|
||||
const IconComponent = icon ? iconMap[icon] : null
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"bg-white dark:bg-gray-800 rounded-lg shadow p-5 border border-gray-200 dark:border-gray-700",
|
||||
"hover:scale-[1.02] hover:shadow-lg transition-all duration-200",
|
||||
variantStyles[variant],
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
{IconComponent && <IconComponent className="h-4 w-4 text-gray-500 dark:text-gray-400" />}
|
||||
<div className="text-sm text-gray-600 dark:text-gray-400 font-medium flex items-center gap-1.5">
|
||||
{label}
|
||||
{helpText && <InfoIcon content={helpText} side="top" />}
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-2xl font-bold text-gray-900 dark:text-white">
|
||||
{value}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -1,823 +0,0 @@
|
|||
"use client"
|
||||
|
||||
import { useState, useRef, useEffect, memo, useMemo } from "react"
|
||||
import {
|
||||
Clock,
|
||||
FlaskConical,
|
||||
Activity,
|
||||
Box,
|
||||
RefreshCw,
|
||||
ChevronDown,
|
||||
FileText,
|
||||
Code,
|
||||
GitCompare,
|
||||
CheckCircle2,
|
||||
XCircle,
|
||||
AlertCircle,
|
||||
BarChart3,
|
||||
} from "lucide-react"
|
||||
import { CodeHighlighter, CODE_STYLE } from "./code-highlighter"
|
||||
import type { TimelineSection, TimelineSectionContent } from "./timeline-types"
|
||||
|
||||
function stripCodeHeader(code: string): string {
|
||||
let lines = code.split("\n")
|
||||
if (lines[0] && /^`{3}[a-z]*(:.*)?$/i.test(lines[0].trim())) {
|
||||
lines = lines.slice(1)
|
||||
}
|
||||
if (lines.length > 0 && lines[lines.length - 1]?.trim() === "```") {
|
||||
lines = lines.slice(0, -1)
|
||||
}
|
||||
return lines.join("\n")
|
||||
}
|
||||
|
||||
interface TimelinePageViewProps {
|
||||
sections: TimelineSection[]
|
||||
totalDuration: number
|
||||
functionName?: string | null
|
||||
filePath?: string | null
|
||||
}
|
||||
|
||||
const TYPE_CONFIG = {
|
||||
test_generation: { icon: FlaskConical },
|
||||
optimization: { icon: Box },
|
||||
line_profiler: { icon: Activity },
|
||||
refinement: { icon: RefreshCw },
|
||||
ranking: { icon: BarChart3 },
|
||||
summary: { icon: CheckCircle2 },
|
||||
}
|
||||
|
||||
function formatTime(ms: number): string {
|
||||
if (ms < 1000) return `${Math.round(ms)}ms`
|
||||
if (ms < 60000) return `${(ms / 1000).toFixed(1)}s`
|
||||
return `${(ms / 60000).toFixed(1)}m`
|
||||
}
|
||||
|
||||
function getStatusIcon(status: string) {
|
||||
switch (status) {
|
||||
case "success":
|
||||
return <CheckCircle2 className="h-4 w-4 text-green-500" />
|
||||
case "failed":
|
||||
return <XCircle className="h-4 w-4 text-red-500" />
|
||||
case "partial":
|
||||
return <AlertCircle className="h-4 w-4 text-yellow-500" />
|
||||
default:
|
||||
return <Clock className="h-4 w-4 text-zinc-400" />
|
||||
}
|
||||
}
|
||||
|
||||
interface ParsedCodeBlock {
|
||||
language: string
|
||||
filename: string | null
|
||||
path: string | null
|
||||
code: string
|
||||
}
|
||||
|
||||
function parseCodeBlock(rawCode: string): ParsedCodeBlock {
|
||||
const markdownMatch = rawCode.match(/^```(\w+)(?::([^\n]+))?\n([\s\S]*?)```\s*$/)
|
||||
if (markdownMatch) {
|
||||
const [, language, path, code] = markdownMatch
|
||||
const filename = path ? path.split("/").pop() || null : null
|
||||
return { language: language || "python", filename, path: path || null, code: code.trimEnd() }
|
||||
}
|
||||
return { language: "python", filename: null, path: null, code: rawCode }
|
||||
}
|
||||
|
||||
function parseAllCodeBlocks(markdown: string): ParsedCodeBlock[] {
|
||||
const files: ParsedCodeBlock[] = []
|
||||
const regex = /```(\w+)(?::([^\n]+))?\n([\s\S]*?)```/g
|
||||
let match
|
||||
|
||||
while ((match = regex.exec(markdown)) !== null) {
|
||||
const [, language, path, code] = match
|
||||
const filename = path ? path.split("/").pop() || null : null
|
||||
files.push({
|
||||
path: path || null,
|
||||
filename,
|
||||
language: language || "python",
|
||||
code: code.trimEnd(),
|
||||
})
|
||||
}
|
||||
|
||||
if (files.length === 0 && markdown.trim()) {
|
||||
return [parseCodeBlock(markdown)]
|
||||
}
|
||||
|
||||
return files
|
||||
}
|
||||
|
||||
function findMatchingFile(
|
||||
files: ParsedCodeBlock[],
|
||||
targetPath: string | null
|
||||
): ParsedCodeBlock | null {
|
||||
if (!targetPath || files.length === 0) return files[0] || null
|
||||
|
||||
const exactMatch = files.find(f => f.path === targetPath)
|
||||
if (exactMatch) return exactMatch
|
||||
|
||||
const targetFilename = targetPath.split("/").pop()
|
||||
const filenameMatch = files.find(f => f.filename === targetFilename)
|
||||
if (filenameMatch) return filenameMatch
|
||||
|
||||
const partialMatch = files.find(f =>
|
||||
f.path && (targetPath.endsWith(f.path) || f.path.endsWith(targetPath))
|
||||
)
|
||||
if (partialMatch) return partialMatch
|
||||
|
||||
return files[0] || null
|
||||
}
|
||||
|
||||
const DiffView = memo(function DiffView({ diff }: { diff: string }) {
|
||||
const lines = diff.split("\n")
|
||||
|
||||
return (
|
||||
<div className="font-mono text-sm bg-zinc-900 overflow-x-auto">
|
||||
{lines.map((line, index) => {
|
||||
const isAddition = line.startsWith("+")
|
||||
const isDeletion = line.startsWith("-")
|
||||
const isHunkHeader = line.startsWith("@@")
|
||||
const isNoNewline = line.startsWith("\\ No newline") || line.startsWith("\\")
|
||||
|
||||
if (index === lines.length - 1 && line === "") return null
|
||||
if ((line === "+" || line === "-") || (isAddition && line.substring(1).trim() === "") || (isDeletion && line.substring(1).trim() === "")) {
|
||||
return null
|
||||
}
|
||||
if (isNoNewline) return null
|
||||
|
||||
let bgClass = ""
|
||||
let textClass = "text-zinc-300"
|
||||
let lineContent = line
|
||||
let indicator: React.ReactNode = null
|
||||
let borderClass = "border-transparent"
|
||||
|
||||
if (isHunkHeader) {
|
||||
bgClass = "bg-blue-900/30"
|
||||
textClass = "text-blue-400"
|
||||
} else if (isAddition) {
|
||||
bgClass = "bg-green-900/40"
|
||||
textClass = "text-green-300"
|
||||
lineContent = line.substring(1)
|
||||
indicator = <span className="text-green-500">+</span>
|
||||
borderClass = "border-green-500"
|
||||
} else if (isDeletion) {
|
||||
bgClass = "bg-red-900/40"
|
||||
textClass = "text-red-300"
|
||||
lineContent = line.substring(1)
|
||||
indicator = <span className="text-red-500">−</span>
|
||||
borderClass = "border-red-500"
|
||||
} else if (line.startsWith(" ")) {
|
||||
lineContent = line.substring(1)
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
key={index}
|
||||
className={`flex ${bgClass} border-l-2 ${borderClass}`}
|
||||
>
|
||||
<div className="w-8 flex-shrink-0 text-right pr-2 select-none">
|
||||
{indicator}
|
||||
</div>
|
||||
<pre className={`flex-1 px-2 py-0.5 ${textClass} whitespace-pre`}>
|
||||
{lineContent || " "}
|
||||
</pre>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
})
|
||||
|
||||
const TestContent = memo(function TestContent({ content }: { content: Extract<TimelineSectionContent, { type: "tests" }> }) {
|
||||
const [showDetails, setShowDetails] = useState(false)
|
||||
const [expandedTest, setExpandedTest] = useState<number | null>(null)
|
||||
const [activeVariant, setActiveVariant] = useState<"generated" | "instrumented" | "instrumentedPerf">("generated")
|
||||
|
||||
const testCount = content.testGroups.length
|
||||
const hasInstrumented = content.testGroups.some(g => g.instrumented)
|
||||
const hasInstrumentedPerf = content.testGroups.some(g => g.instrumentedPerf)
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<CheckCircle2 className="h-5 w-5 text-green-500" />
|
||||
<span className="text-base font-medium text-zinc-700 dark:text-zinc-300">
|
||||
{testCount} test{testCount !== 1 ? "s" : ""} generated
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
{content.testFramework && (
|
||||
<span className="text-xs px-1.5 py-0.5 bg-zinc-100 dark:bg-zinc-700 text-zinc-600 dark:text-zinc-300 rounded">
|
||||
{content.testFramework}
|
||||
</span>
|
||||
)}
|
||||
{hasInstrumented && (
|
||||
<span className="text-xs px-1.5 py-0.5 bg-zinc-100 dark:bg-zinc-700 text-zinc-600 dark:text-zinc-300 rounded">
|
||||
+behavior
|
||||
</span>
|
||||
)}
|
||||
{hasInstrumentedPerf && (
|
||||
<span className="text-xs px-1.5 py-0.5 bg-zinc-100 dark:bg-zinc-700 text-zinc-600 dark:text-zinc-300 rounded">
|
||||
+perf
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setShowDetails(!showDetails)}
|
||||
className="px-3 py-1.5 text-xs rounded-md bg-zinc-100 dark:bg-zinc-800 text-zinc-600 dark:text-zinc-400 hover:bg-zinc-200 dark:hover:bg-zinc-700 transition-colors flex items-center gap-1.5"
|
||||
>
|
||||
{showDetails ? "Hide Details" : "View Details"}
|
||||
<ChevronDown className={`h-3.5 w-3.5 transition-transform duration-200 ${showDetails ? "" : "-rotate-90"}`} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{showDetails && (
|
||||
<div className="space-y-3 pt-2 border-t border-zinc-200 dark:border-zinc-700">
|
||||
{content.testGroups.map((group) => {
|
||||
const isExpanded = expandedTest === group.index
|
||||
const hasMultipleVariants = [group.generated, group.instrumented, group.instrumentedPerf].filter(Boolean).length > 1
|
||||
const currentCode = activeVariant === "generated" ? group.generated
|
||||
: activeVariant === "instrumented" ? group.instrumented
|
||||
: group.instrumentedPerf
|
||||
|
||||
return (
|
||||
<div key={group.index} className="rounded-md border border-zinc-200 dark:border-zinc-700 overflow-hidden">
|
||||
<button
|
||||
onClick={() => setExpandedTest(isExpanded ? null : group.index)}
|
||||
className="w-full px-3 py-2 bg-zinc-100 dark:bg-zinc-800 flex items-center justify-between hover:bg-zinc-200 dark:hover:bg-zinc-700 transition-colors"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<FlaskConical className="h-3.5 w-3.5 text-zinc-400" />
|
||||
<span className="text-sm font-medium text-zinc-700 dark:text-zinc-300">
|
||||
Test {group.index}
|
||||
</span>
|
||||
<div className="flex items-center gap-1">
|
||||
{group.generated && (
|
||||
<span className="text-[10px] px-1 py-0.5 bg-zinc-200 dark:bg-zinc-600 text-zinc-600 dark:text-zinc-300 rounded">
|
||||
gen
|
||||
</span>
|
||||
)}
|
||||
{group.instrumented && (
|
||||
<span className="text-[10px] px-1 py-0.5 bg-zinc-200 dark:bg-zinc-600 text-zinc-600 dark:text-zinc-300 rounded">
|
||||
beh
|
||||
</span>
|
||||
)}
|
||||
{group.instrumentedPerf && (
|
||||
<span className="text-[10px] px-1 py-0.5 bg-zinc-200 dark:bg-zinc-600 text-zinc-600 dark:text-zinc-300 rounded">
|
||||
perf
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xs text-zinc-500">
|
||||
{group.generated?.lines ?? group.instrumented?.lines ?? group.instrumentedPerf?.lines} lines
|
||||
</span>
|
||||
<ChevronDown className={`h-4 w-4 text-zinc-400 transition-transform duration-200 ${isExpanded ? "" : "-rotate-90"}`} />
|
||||
</div>
|
||||
</button>
|
||||
|
||||
{isExpanded && (
|
||||
<div>
|
||||
{hasMultipleVariants && (
|
||||
<div className="flex items-center gap-1 px-3 py-2 bg-zinc-50 dark:bg-zinc-900 border-b border-zinc-200 dark:border-zinc-700">
|
||||
{group.generated && (
|
||||
<button
|
||||
onClick={() => setActiveVariant("generated")}
|
||||
className={`px-2 py-1 text-xs rounded transition-colors ${
|
||||
activeVariant === "generated"
|
||||
? "bg-zinc-200 dark:bg-zinc-700 text-zinc-900 dark:text-zinc-100"
|
||||
: "text-zinc-500 hover:text-zinc-700 dark:hover:text-zinc-300"
|
||||
}`}
|
||||
>
|
||||
Generated
|
||||
</button>
|
||||
)}
|
||||
{group.instrumented && (
|
||||
<button
|
||||
onClick={() => setActiveVariant("instrumented")}
|
||||
className={`px-2 py-1 text-xs rounded transition-colors ${
|
||||
activeVariant === "instrumented"
|
||||
? "bg-zinc-200 dark:bg-zinc-700 text-zinc-900 dark:text-zinc-100"
|
||||
: "text-zinc-500 hover:text-zinc-700 dark:hover:text-zinc-300"
|
||||
}`}
|
||||
>
|
||||
Behavior
|
||||
</button>
|
||||
)}
|
||||
{group.instrumentedPerf && (
|
||||
<button
|
||||
onClick={() => setActiveVariant("instrumentedPerf")}
|
||||
className={`px-2 py-1 text-xs rounded transition-colors ${
|
||||
activeVariant === "instrumentedPerf"
|
||||
? "bg-zinc-200 dark:bg-zinc-700 text-zinc-900 dark:text-zinc-100"
|
||||
: "text-zinc-500 hover:text-zinc-700 dark:hover:text-zinc-300"
|
||||
}`}
|
||||
>
|
||||
Perf
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="max-h-[50vh] overflow-y-auto">
|
||||
{currentCode ? (
|
||||
<CodeHighlighter language="python" code={currentCode.code} customStyle={CODE_STYLE} />
|
||||
) : (
|
||||
<div className="p-4 text-sm text-zinc-500 dark:text-zinc-400 italic">
|
||||
No {activeVariant === "generated" ? "generated" : activeVariant === "instrumented" ? "instrumented behavior" : "instrumented perf"} test available
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
})
|
||||
|
||||
const CandidateContent = memo(function CandidateContent({
|
||||
content,
|
||||
isActive,
|
||||
}: {
|
||||
content: Extract<TimelineSectionContent, { type: "candidate" | "refinement" }>
|
||||
isActive: boolean
|
||||
}) {
|
||||
const [viewMode, setViewMode] = useState<"code" | "diff">("diff")
|
||||
const [selectedFileIndex, setSelectedFileIndex] = useState(0)
|
||||
const [unifiedDiff, setUnifiedDiff] = useState<string | null>(null)
|
||||
const [diffLoading, setDiffLoading] = useState(false)
|
||||
|
||||
const originalCode = content.type === "refinement" ? content.parentCode : content.originalCode
|
||||
|
||||
const candidateFiles = useMemo(() => parseAllCodeBlocks(content.code), [content.code])
|
||||
const originalFiles = useMemo(() => originalCode ? parseAllCodeBlocks(originalCode) : [], [originalCode])
|
||||
|
||||
const selectedCandidateFile = candidateFiles[selectedFileIndex] || candidateFiles[0]
|
||||
|
||||
const matchingOriginalFile = useMemo(() => {
|
||||
if (!selectedCandidateFile || originalFiles.length === 0) return null
|
||||
return findMatchingFile(originalFiles, selectedCandidateFile.path)
|
||||
}, [selectedCandidateFile, originalFiles])
|
||||
|
||||
useEffect(() => {
|
||||
setUnifiedDiff(null)
|
||||
}, [selectedFileIndex])
|
||||
|
||||
useEffect(() => {
|
||||
if (viewMode !== "diff" || !matchingOriginalFile || !selectedCandidateFile || unifiedDiff !== null) {
|
||||
return
|
||||
}
|
||||
|
||||
setDiffLoading(true)
|
||||
import("diff").then(({ createTwoFilesPatch }) => {
|
||||
const filename = selectedCandidateFile.filename || matchingOriginalFile.filename || "code.py"
|
||||
const diff = createTwoFilesPatch(
|
||||
`a/${filename}`,
|
||||
`b/${filename}`,
|
||||
matchingOriginalFile.code,
|
||||
selectedCandidateFile.code,
|
||||
"",
|
||||
"",
|
||||
{ context: 3 }
|
||||
)
|
||||
|
||||
const lines = diff.split("\n")
|
||||
const hunkStartIndex = lines.findIndex(line => line.startsWith("@@"))
|
||||
setUnifiedDiff(hunkStartIndex > 0 ? lines.slice(hunkStartIndex).join("\n") : diff)
|
||||
setDiffLoading(false)
|
||||
}).catch(error => {
|
||||
console.error("Failed to load diff library:", error)
|
||||
setDiffLoading(false)
|
||||
})
|
||||
}, [viewMode, matchingOriginalFile, selectedCandidateFile, unifiedDiff])
|
||||
|
||||
const hasDiff = matchingOriginalFile !== null
|
||||
const hasMultipleFiles = candidateFiles.length > 1
|
||||
|
||||
const codeContainerStyle = useMemo(
|
||||
() => ({ maxHeight: isActive ? "70vh" : "200px" }),
|
||||
[isActive]
|
||||
)
|
||||
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
{content.rank != null && (
|
||||
<span className="text-xs px-1.5 py-0.5 bg-zinc-100 dark:bg-zinc-700 text-zinc-600 dark:text-zinc-300 rounded">
|
||||
#{content.rank}
|
||||
</span>
|
||||
)}
|
||||
{content.isBest && (
|
||||
<span className="text-xs px-1.5 py-0.5 bg-zinc-200 dark:bg-zinc-600 text-zinc-700 dark:text-zinc-200 rounded font-medium">
|
||||
Best
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{content.explanation && (
|
||||
<p className="text-sm text-zinc-600 dark:text-zinc-400 leading-relaxed">
|
||||
{content.explanation}
|
||||
</p>
|
||||
)}
|
||||
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
{hasDiff && (
|
||||
<div className="flex items-center gap-1">
|
||||
<button
|
||||
onClick={() => setViewMode("code")}
|
||||
className={`px-3 py-1.5 text-xs rounded-md flex items-center gap-1.5 transition-colors ${
|
||||
viewMode === "code"
|
||||
? "bg-zinc-800 text-white"
|
||||
: "bg-zinc-100 dark:bg-zinc-800 text-zinc-600 dark:text-zinc-400 hover:bg-zinc-200 dark:hover:bg-zinc-700"
|
||||
}`}
|
||||
>
|
||||
<Code className="h-3.5 w-3.5" />
|
||||
Code
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setViewMode("diff")}
|
||||
className={`px-3 py-1.5 text-xs rounded-md flex items-center gap-1.5 transition-colors ${
|
||||
viewMode === "diff"
|
||||
? "bg-zinc-800 text-white"
|
||||
: "bg-zinc-100 dark:bg-zinc-800 text-zinc-600 dark:text-zinc-400 hover:bg-zinc-200 dark:hover:bg-zinc-700"
|
||||
}`}
|
||||
>
|
||||
<GitCompare className="h-3.5 w-3.5" />
|
||||
Diff
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{hasMultipleFiles && (
|
||||
<select
|
||||
value={selectedFileIndex}
|
||||
onChange={(e) => setSelectedFileIndex(Number(e.target.value))}
|
||||
className="px-2 py-1.5 text-xs rounded-md bg-zinc-100 dark:bg-zinc-800 text-zinc-600 dark:text-zinc-300 border border-zinc-200 dark:border-zinc-700"
|
||||
>
|
||||
{candidateFiles.map((file, index) => (
|
||||
<option key={index} value={index}>
|
||||
{file.filename || file.path || `File ${index + 1}`}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{viewMode === "code" ? (
|
||||
selectedCandidateFile ? (
|
||||
<div className="rounded-md border border-zinc-200 dark:border-zinc-700 overflow-hidden">
|
||||
<div className="px-3 py-2 bg-zinc-100 dark:bg-zinc-800 flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<FileText className="h-3.5 w-3.5 text-zinc-400" />
|
||||
<span className="text-sm font-mono font-medium text-zinc-700 dark:text-zinc-300">
|
||||
{selectedCandidateFile.filename || "Code"}
|
||||
</span>
|
||||
{selectedCandidateFile.path && selectedCandidateFile.path !== selectedCandidateFile.filename && (
|
||||
<span className="text-xs text-zinc-500 dark:text-zinc-400">
|
||||
({selectedCandidateFile.path})
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<span className="text-xs text-zinc-500">
|
||||
{selectedCandidateFile.code.split("\n").length} lines
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
className="overflow-y-auto transition-all duration-500 ease-out"
|
||||
style={codeContainerStyle}
|
||||
>
|
||||
<CodeHighlighter language={selectedCandidateFile.language} code={selectedCandidateFile.code} customStyle={CODE_STYLE} />
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-sm text-zinc-500 dark:text-zinc-400 italic p-4 bg-zinc-50 dark:bg-zinc-800/50 rounded-md">
|
||||
No code available
|
||||
</div>
|
||||
)
|
||||
) : diffLoading ? (
|
||||
<div className="rounded-md border border-zinc-200 dark:border-zinc-700 overflow-hidden">
|
||||
<div className="p-4 bg-zinc-900 animate-pulse">
|
||||
<div className="h-4 bg-zinc-700 rounded w-3/4 mb-2" />
|
||||
<div className="h-4 bg-zinc-700 rounded w-1/2 mb-2" />
|
||||
<div className="h-4 bg-zinc-700 rounded w-2/3" />
|
||||
</div>
|
||||
</div>
|
||||
) : unifiedDiff ? (
|
||||
<div
|
||||
className="rounded-md border border-zinc-200 dark:border-zinc-700 overflow-hidden overflow-y-auto transition-all duration-500"
|
||||
style={codeContainerStyle}
|
||||
>
|
||||
<DiffView diff={unifiedDiff} />
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-sm text-zinc-500 dark:text-zinc-400 italic p-4 bg-zinc-50 dark:bg-zinc-800/50 rounded-md">
|
||||
No original code available for comparison
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
})
|
||||
|
||||
const RankingContent = memo(function RankingContent({ content }: { content: Extract<TimelineSectionContent, { type: "ranking" }> }) {
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{content.explanation && (
|
||||
<div className="p-3 bg-zinc-50 dark:bg-zinc-800/50 rounded border border-zinc-200 dark:border-zinc-700">
|
||||
<p className="text-sm text-zinc-600 dark:text-zinc-400 whitespace-pre-wrap">
|
||||
{content.explanation}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{content.rankings.length >= 1 && (
|
||||
<div className="space-y-4">
|
||||
{content.rankings.map((item) => (
|
||||
<div
|
||||
key={item.id}
|
||||
className={`rounded border ${
|
||||
item.isBest
|
||||
? "border-emerald-300 dark:border-emerald-700"
|
||||
: "border-zinc-300 dark:border-zinc-600"
|
||||
}`}
|
||||
>
|
||||
<div className={`flex items-center gap-2 px-3 py-2 border-b flex-wrap ${
|
||||
item.isBest
|
||||
? "bg-emerald-50 dark:bg-emerald-900/20 border-emerald-200 dark:border-emerald-800"
|
||||
: "bg-zinc-100 dark:bg-zinc-700 border-zinc-200 dark:border-zinc-700"
|
||||
}`}>
|
||||
<span className="text-sm font-medium text-zinc-900 dark:text-white">
|
||||
{item.label}
|
||||
</span>
|
||||
<span className="bg-indigo-100 dark:bg-indigo-900 text-indigo-800 dark:text-indigo-200 text-xs px-2 py-0.5 rounded font-semibold">
|
||||
Rank #{item.rank}
|
||||
</span>
|
||||
{item.isBest && (
|
||||
<span className="bg-emerald-100 dark:bg-emerald-900 text-emerald-800 dark:text-emerald-200 text-xs px-2 py-0.5 rounded font-semibold">
|
||||
Best
|
||||
</span>
|
||||
)}
|
||||
{item.isBest && content.usedForPr && (
|
||||
<span className="bg-blue-100 dark:bg-blue-900 text-blue-800 dark:text-blue-200 text-xs px-2 py-0.5 rounded font-semibold">
|
||||
Used for PR
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="max-h-80 overflow-auto">
|
||||
<CodeHighlighter code={stripCodeHeader(item.code)} language="python" customStyle={CODE_STYLE} />
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
</div>
|
||||
)
|
||||
})
|
||||
|
||||
const SummaryContent = memo(function SummaryContent({ content }: { content: Extract<TimelineSectionContent, { type: "summary" }> }) {
|
||||
const { metrics } = content
|
||||
return (
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||
<div className="p-4 bg-zinc-50 dark:bg-zinc-800/50 rounded-md">
|
||||
<div className="text-xs text-zinc-500 dark:text-zinc-400 mb-1">Total Duration</div>
|
||||
<div className="text-xl font-semibold text-zinc-900 dark:text-white">
|
||||
{formatTime(metrics.totalDuration)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-4 bg-zinc-50 dark:bg-zinc-800/50 rounded-md">
|
||||
<div className="text-xs text-zinc-500 dark:text-zinc-400 mb-1">Total Cost</div>
|
||||
<div className="text-xl font-semibold text-zinc-900 dark:text-white">
|
||||
${metrics.totalCost.toFixed(4)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-4 bg-zinc-50 dark:bg-zinc-800/50 rounded-md">
|
||||
<div className="text-xs text-zinc-500 dark:text-zinc-400 mb-1">Total Tokens</div>
|
||||
<div className="text-xl font-semibold text-zinc-900 dark:text-white">
|
||||
{metrics.totalTokens.toLocaleString()}
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-4 bg-zinc-50 dark:bg-zinc-800/50 rounded-md">
|
||||
<div className="text-xs text-zinc-500 dark:text-zinc-400 mb-1">Candidates</div>
|
||||
<div className="text-xl font-semibold text-zinc-900 dark:text-white">
|
||||
{metrics.candidatesCount}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
|
||||
const TimelineSectionCard = memo(function TimelineSectionCard({
|
||||
section,
|
||||
isActive,
|
||||
index,
|
||||
totalSections,
|
||||
}: {
|
||||
section: TimelineSection
|
||||
isActive: boolean
|
||||
index: number
|
||||
totalSections: number
|
||||
}) {
|
||||
const config = TYPE_CONFIG[section.type]
|
||||
const Icon = config.icon
|
||||
|
||||
return (
|
||||
<div className={`relative transition-opacity duration-200 ${isActive ? "opacity-100" : "opacity-60"}`}>
|
||||
<div className="absolute right-6 top-0 bottom-0 w-px bg-zinc-200 dark:bg-zinc-700" />
|
||||
|
||||
<div className="mr-14 mb-6">
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<span className="text-xs font-mono text-zinc-500 dark:text-zinc-400">
|
||||
+{formatTime(section.timestamp)}
|
||||
</span>
|
||||
{section.duration && (
|
||||
<>
|
||||
<span className="text-zinc-300 dark:text-zinc-600">·</span>
|
||||
<span className="text-xs text-zinc-400 dark:text-zinc-500">
|
||||
{formatTime(section.duration)}
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
<div className="flex-1" />
|
||||
<span className="text-xs text-zinc-400 dark:text-zinc-500">
|
||||
{index + 1}/{totalSections}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={`rounded-md border overflow-hidden transition-colors duration-200 ${
|
||||
isActive
|
||||
? "border-zinc-300 dark:border-zinc-600 bg-white dark:bg-zinc-800"
|
||||
: "border-zinc-200 dark:border-zinc-700 bg-zinc-50 dark:bg-zinc-800/50"
|
||||
}`}
|
||||
>
|
||||
<div className="px-4 py-3 border-b border-zinc-200 dark:border-zinc-700 bg-zinc-50 dark:bg-zinc-800">
|
||||
<div className="flex items-center gap-3">
|
||||
<Icon className="h-4 w-4 text-zinc-500 dark:text-zinc-400" />
|
||||
<div className="flex-1">
|
||||
<h3 className="text-sm font-medium text-zinc-900 dark:text-zinc-100">
|
||||
{section.title}
|
||||
</h3>
|
||||
{section.subtitle && (
|
||||
<p className="text-xs text-zinc-500 dark:text-zinc-400 mt-0.5">
|
||||
{section.subtitle}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
{getStatusIcon(section.status)}
|
||||
{section.model && (
|
||||
<span className="text-xs px-1.5 py-0.5 bg-zinc-100 dark:bg-zinc-700 text-zinc-600 dark:text-zinc-300 rounded">
|
||||
{section.model}
|
||||
</span>
|
||||
)}
|
||||
{section.cost != null && (
|
||||
<span className="text-xs text-zinc-500 dark:text-zinc-400">
|
||||
${section.cost.toFixed(4)}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-4 bg-white dark:bg-zinc-800">
|
||||
{section.content.type === "tests" && <TestContent content={section.content} />}
|
||||
{(section.content.type === "candidate" || section.content.type === "refinement") && (
|
||||
<CandidateContent content={section.content} isActive={isActive} />
|
||||
)}
|
||||
{section.content.type === "ranking" && <RankingContent content={section.content} />}
|
||||
{section.content.type === "summary" && <SummaryContent content={section.content} />}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
|
||||
export const TimelinePageView = memo(function TimelinePageView({
|
||||
sections,
|
||||
totalDuration,
|
||||
functionName,
|
||||
filePath,
|
||||
}: TimelinePageViewProps) {
|
||||
const [activeIndex, setActiveIndex] = useState(0)
|
||||
const sectionRefs = useRef<(HTMLDivElement | null)[]>([])
|
||||
const rafId = useRef<number | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
const handleScroll = () => {
|
||||
if (rafId.current !== null) return
|
||||
rafId.current = requestAnimationFrame(() => {
|
||||
rafId.current = null
|
||||
const scrollTarget = window.innerHeight * 0.35
|
||||
|
||||
let closestIndex = 0
|
||||
let closestDistance = Infinity
|
||||
|
||||
sectionRefs.current.forEach((ref, index) => {
|
||||
if (ref) {
|
||||
const rect = ref.getBoundingClientRect()
|
||||
const sectionMiddle = rect.top + rect.height / 2
|
||||
const distance = Math.abs(sectionMiddle - scrollTarget)
|
||||
|
||||
if (distance < closestDistance) {
|
||||
closestDistance = distance
|
||||
closestIndex = index
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
setActiveIndex(closestIndex)
|
||||
})
|
||||
}
|
||||
|
||||
window.addEventListener("scroll", handleScroll, { passive: true })
|
||||
handleScroll()
|
||||
return () => {
|
||||
window.removeEventListener("scroll", handleScroll)
|
||||
if (rafId.current !== null) {
|
||||
cancelAnimationFrame(rafId.current)
|
||||
}
|
||||
}
|
||||
}, [sections.length])
|
||||
|
||||
if (sections.length === 0) {
|
||||
return (
|
||||
<div className="flex items-center justify-center py-20 text-zinc-400">
|
||||
No timeline data available
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
<div className="sticky top-0 z-30 bg-white/80 dark:bg-zinc-900/80 backdrop-blur-sm border-b border-zinc-200 dark:border-zinc-700 mb-6">
|
||||
<div className="max-w-6xl mx-auto px-4 py-3">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<div>
|
||||
<h2 className="text-sm font-medium text-zinc-900 dark:text-white">
|
||||
Optimization Timeline
|
||||
</h2>
|
||||
{functionName && (
|
||||
<p className="text-xs text-zinc-500 dark:text-zinc-400">
|
||||
{functionName}
|
||||
{filePath && <span className="ml-1 opacity-60">in {filePath}</span>}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<span className="text-xs text-zinc-500 dark:text-zinc-400">
|
||||
{activeIndex + 1} of {sections.length} · {formatTime(totalDuration)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="h-1 bg-zinc-200 dark:bg-zinc-700 rounded-full overflow-hidden">
|
||||
<div
|
||||
className="h-full bg-zinc-400 dark:bg-zinc-500 transition-all duration-200"
|
||||
style={{ width: `${((activeIndex + 1) / sections.length) * 100}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="max-w-6xl mx-auto px-4 pb-20 relative">
|
||||
<div className="sticky top-1/2 -translate-y-1/2 z-20 pointer-events-none h-0">
|
||||
<div className="absolute right-4 -top-2">
|
||||
<div className="w-4 h-4 rounded-full bg-zinc-400 dark:bg-zinc-500 border-2 border-white dark:border-zinc-900" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{sections.map((section, index) => (
|
||||
<div
|
||||
key={section.id}
|
||||
ref={(el) => { sectionRefs.current[index] = el }}
|
||||
className="scroll-mt-24"
|
||||
>
|
||||
<TimelineSectionCard
|
||||
section={section}
|
||||
isActive={index === activeIndex}
|
||||
index={index}
|
||||
totalSections={sections.length}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
|
||||
<div className="relative">
|
||||
<div className="absolute right-6 top-0 h-6 w-px bg-zinc-200 dark:bg-zinc-700" />
|
||||
<div className="absolute right-4 top-6 w-4 h-4 rounded-full bg-zinc-300 dark:bg-zinc-600 border-2 border-white dark:border-zinc-900 z-10" />
|
||||
<div className="mr-14 py-4 text-right">
|
||||
<span className="text-xs text-zinc-500 dark:text-zinc-400">
|
||||
End
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
|
|
@ -1,289 +0,0 @@
|
|||
export interface TimelineSection {
|
||||
id: string
|
||||
type: "test_generation" | "optimization" | "line_profiler" | "refinement" | "ranking" | "summary"
|
||||
title: string
|
||||
subtitle?: string
|
||||
timestamp: number
|
||||
duration?: number
|
||||
status: "success" | "failed" | "partial" | "pending"
|
||||
model?: string | null
|
||||
cost?: number | null
|
||||
tokens?: number | null
|
||||
content: TimelineSectionContent
|
||||
}
|
||||
|
||||
export interface TestGroup {
|
||||
index: number
|
||||
generated?: { code: string; lines: number }
|
||||
instrumented?: { code: string; lines: number }
|
||||
instrumentedPerf?: { code: string; lines: number }
|
||||
}
|
||||
|
||||
export type TimelineSectionContent =
|
||||
| { type: "tests"; testGroups: TestGroup[]; testFramework?: string }
|
||||
| { type: "candidate"; code: string; originalCode: string | null; explanation?: string; rank?: number; isBest?: boolean }
|
||||
| { type: "refinement"; code: string; parentCode: string | null; explanation?: string; rank?: number; isBest?: boolean }
|
||||
| { type: "ranking"; explanation: string; rankings: Array<{ id: string; rank: number; label: string; code: string; isBest: boolean }>; usedForPr: boolean }
|
||||
| { type: "summary"; metrics: { totalCost: number; totalTokens: number; totalDuration: number; candidatesCount: number } }
|
||||
|
||||
export interface TransformInput {
|
||||
calls: Array<{
|
||||
id: string
|
||||
call_type: string | null
|
||||
model_name: string | null
|
||||
status: string
|
||||
latency_ms: number | null
|
||||
llm_cost: number | null
|
||||
total_tokens: number | null
|
||||
created_at: Date
|
||||
context: { call_sequence?: number } | null
|
||||
}>
|
||||
optimizationCandidates: Array<{
|
||||
id: string
|
||||
code: string
|
||||
explanation?: string
|
||||
index: number
|
||||
}>
|
||||
lineProfilerCandidates: Array<{
|
||||
id: string
|
||||
code: string
|
||||
explanation?: string
|
||||
index: number
|
||||
}>
|
||||
refinementCandidates: Array<{
|
||||
id: string
|
||||
code: string
|
||||
explanation?: string
|
||||
parentId: string | null
|
||||
index: number
|
||||
}>
|
||||
generatedTests: Array<{ code: string; index: number }>
|
||||
instrumentedTests: Array<{ code: string; index: number }>
|
||||
instrumentedPerfTests: Array<{ code: string; index: number }>
|
||||
originalCode: string | null
|
||||
testFramework: string | null
|
||||
candidateRankMap: Record<string, number>
|
||||
bestCandidateId: string | null
|
||||
rankingExplanation: string | null
|
||||
usedForPr: boolean
|
||||
}
|
||||
|
||||
export function transformToTimelineSections(input: TransformInput): { sections: TimelineSection[]; totalDuration: number } {
|
||||
const { calls, optimizationCandidates, lineProfilerCandidates, refinementCandidates, generatedTests, instrumentedTests, instrumentedPerfTests, originalCode, testFramework, candidateRankMap, bestCandidateId, rankingExplanation, usedForPr } = input
|
||||
|
||||
if (calls.length === 0) {
|
||||
return { sections: [], totalDuration: 0 }
|
||||
}
|
||||
|
||||
const timestamps = calls.map(c => new Date(c.created_at).getTime())
|
||||
const minTime = Math.min(...timestamps)
|
||||
const maxTime = Math.max(...timestamps)
|
||||
const maxLatency = Math.max(...calls.map(c => c.latency_ms ?? 0))
|
||||
const totalDuration = maxTime - minTime + maxLatency
|
||||
|
||||
const sections: TimelineSection[] = []
|
||||
|
||||
const maxTestIndex = Math.max(
|
||||
generatedTests.length,
|
||||
instrumentedTests.length,
|
||||
instrumentedPerfTests.length
|
||||
)
|
||||
|
||||
const testGroups: TestGroup[] = []
|
||||
for (let i = 1; i <= maxTestIndex; i++) {
|
||||
const generated = generatedTests.find(t => t.index === i)
|
||||
const instrumented = instrumentedTests.find(t => t.index === i)
|
||||
const instrumentedPerf = instrumentedPerfTests.find(t => t.index === i)
|
||||
|
||||
if (generated || instrumented || instrumentedPerf) {
|
||||
testGroups.push({
|
||||
index: i,
|
||||
generated: generated ? { code: generated.code, lines: generated.code.split("\n").length } : undefined,
|
||||
instrumented: instrumented ? { code: instrumented.code, lines: instrumented.code.split("\n").length } : undefined,
|
||||
instrumentedPerf: instrumentedPerf ? { code: instrumentedPerf.code, lines: instrumentedPerf.code.split("\n").length } : undefined,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const testCalls = calls.filter(c => c.call_type === "test_generation")
|
||||
if (testCalls.length > 0 || testGroups.length > 0) {
|
||||
const firstTestCall = testCalls[0]
|
||||
const firstTimestamp = firstTestCall ? new Date(firstTestCall.created_at).getTime() - minTime : 0
|
||||
const totalTestDuration = testCalls.reduce((sum, c) => sum + (c.latency_ms ?? 0), 0)
|
||||
const totalTestCost = testCalls.reduce((sum, c) => sum + (c.llm_cost ?? 0), 0)
|
||||
const totalTestTokens = testCalls.reduce((sum, c) => sum + (c.total_tokens ?? 0), 0)
|
||||
const allSuccess = testCalls.length === 0 || testCalls.every(c => c.status === "success")
|
||||
const anyFailed = testCalls.some(c => c.status === "failed")
|
||||
|
||||
const subtitle = testFramework
|
||||
? `${testGroups.length} test${testGroups.length > 1 ? "s" : ""} using ${testFramework}`
|
||||
: `${testGroups.length} test${testGroups.length > 1 ? "s" : ""} generated`
|
||||
|
||||
sections.push({
|
||||
id: firstTestCall ? `tests-${firstTestCall.id}` : "tests",
|
||||
type: "test_generation",
|
||||
title: "Test Generation",
|
||||
subtitle,
|
||||
timestamp: firstTimestamp,
|
||||
duration: totalTestDuration,
|
||||
status: allSuccess ? "success" : anyFailed ? "failed" : "partial",
|
||||
model: firstTestCall?.model_name ?? null,
|
||||
cost: totalTestCost,
|
||||
tokens: totalTestTokens,
|
||||
content: {
|
||||
type: "tests",
|
||||
testGroups,
|
||||
testFramework: testFramework ?? undefined,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const callIndexByType = new Map<string, number>()
|
||||
for (const call of calls) {
|
||||
const timestamp = new Date(call.created_at).getTime() - minTime
|
||||
const callType = call.call_type || "unknown"
|
||||
const typeIndex = callIndexByType.get(callType) ?? 0
|
||||
callIndexByType.set(callType, typeIndex + 1)
|
||||
|
||||
if (callType === "optimization") {
|
||||
const optIndex = typeIndex
|
||||
const candidate = optimizationCandidates[optIndex]
|
||||
if (candidate) {
|
||||
const rank = candidateRankMap[candidate.id]
|
||||
sections.push({
|
||||
id: call.id,
|
||||
type: "optimization",
|
||||
title: `Optimization Candidate ${candidate.index}`,
|
||||
timestamp,
|
||||
duration: call.latency_ms ?? undefined,
|
||||
status: call.status === "success" ? "success" : call.status === "failed" ? "failed" : "partial",
|
||||
model: call.model_name,
|
||||
cost: call.llm_cost,
|
||||
tokens: call.total_tokens,
|
||||
content: {
|
||||
type: "candidate",
|
||||
code: candidate.code,
|
||||
originalCode,
|
||||
explanation: candidate.explanation,
|
||||
rank,
|
||||
isBest: candidate.id === bestCandidateId,
|
||||
},
|
||||
})
|
||||
}
|
||||
} else if (callType === "line_profiler") {
|
||||
const lpIndex = typeIndex
|
||||
const candidate = lineProfilerCandidates[lpIndex]
|
||||
if (candidate) {
|
||||
const rank = candidateRankMap[candidate.id]
|
||||
sections.push({
|
||||
id: call.id,
|
||||
type: "line_profiler",
|
||||
title: `Line Profiler Candidate ${candidate.index}`,
|
||||
subtitle: "Guided by profiling data",
|
||||
timestamp,
|
||||
duration: call.latency_ms ?? undefined,
|
||||
status: call.status === "success" ? "success" : call.status === "failed" ? "failed" : "partial",
|
||||
model: call.model_name,
|
||||
cost: call.llm_cost,
|
||||
tokens: call.total_tokens,
|
||||
content: {
|
||||
type: "candidate",
|
||||
code: candidate.code,
|
||||
originalCode,
|
||||
explanation: candidate.explanation,
|
||||
rank,
|
||||
isBest: candidate.id === bestCandidateId,
|
||||
},
|
||||
})
|
||||
}
|
||||
} else if (callType === "refinement") {
|
||||
const refIndex = typeIndex
|
||||
const candidate = refinementCandidates[refIndex]
|
||||
if (candidate) {
|
||||
const rank = candidateRankMap[candidate.id]
|
||||
const parentCandidate = [...optimizationCandidates, ...lineProfilerCandidates, ...refinementCandidates].find(c => c.id === candidate.parentId)
|
||||
const parentLabel = parentCandidate
|
||||
? (parentCandidate as { source?: string }).source === "REFINE"
|
||||
? `From Refinement ${parentCandidate.index}`
|
||||
: `From Candidate ${parentCandidate.index}`
|
||||
: undefined
|
||||
sections.push({
|
||||
id: call.id,
|
||||
type: "refinement",
|
||||
title: `Refinement ${candidate.index}`,
|
||||
subtitle: parentLabel,
|
||||
timestamp,
|
||||
duration: call.latency_ms ?? undefined,
|
||||
status: call.status === "success" ? "success" : call.status === "failed" ? "failed" : "partial",
|
||||
model: call.model_name,
|
||||
cost: call.llm_cost,
|
||||
tokens: call.total_tokens,
|
||||
content: {
|
||||
type: "refinement",
|
||||
code: candidate.code,
|
||||
parentCode: parentCandidate?.code ?? originalCode,
|
||||
explanation: candidate.explanation,
|
||||
rank,
|
||||
isBest: candidate.id === bestCandidateId,
|
||||
},
|
||||
})
|
||||
}
|
||||
} else if (callType === "ranking") {
|
||||
const allCandidates = [...optimizationCandidates, ...lineProfilerCandidates, ...refinementCandidates]
|
||||
const rankings = Object.entries(candidateRankMap)
|
||||
.sort(([, a], [, b]) => a - b)
|
||||
.map(([id]) => {
|
||||
const cand = allCandidates.find(c => c.id === id)
|
||||
if (!cand) return null
|
||||
const source = (cand as { source?: string }).source
|
||||
const prefix = source === "REFINE" ? "Refinement" : source === "OPTIMIZE_LP" ? "LP Candidate" : "Candidate"
|
||||
return { id, rank: 0, label: `${prefix} ${cand.index}`, code: cand.code, isBest: false }
|
||||
})
|
||||
.filter((r): r is NonNullable<typeof r> => r !== null)
|
||||
.map((r, index) => ({ ...r, rank: index + 1, isBest: index === 0 }))
|
||||
|
||||
sections.push({
|
||||
id: call.id,
|
||||
type: "ranking",
|
||||
title: "Candidate Ranking",
|
||||
subtitle: "Selecting the best optimization",
|
||||
timestamp,
|
||||
duration: call.latency_ms ?? undefined,
|
||||
status: call.status === "success" ? "success" : call.status === "failed" ? "failed" : "partial",
|
||||
model: call.model_name,
|
||||
cost: call.llm_cost,
|
||||
tokens: call.total_tokens,
|
||||
content: {
|
||||
type: "ranking",
|
||||
explanation: rankingExplanation ?? "",
|
||||
rankings,
|
||||
usedForPr,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const typeOrder: Record<string, number> = {
|
||||
test_generation: 0,
|
||||
optimization: 1,
|
||||
line_profiler: 2,
|
||||
refinement: 3,
|
||||
ranking: 4,
|
||||
summary: 5,
|
||||
}
|
||||
|
||||
sections.sort((a, b) => {
|
||||
const orderA = typeOrder[a.type] ?? 99
|
||||
const orderB = typeOrder[b.type] ?? 99
|
||||
if (orderA !== orderB) return orderA - orderB
|
||||
const candidateTypes = ["optimization", "line_profiler", "refinement"]
|
||||
if (candidateTypes.includes(a.type)) {
|
||||
const indexA = parseInt(a.title.match(/\d+$/)?.[0] ?? "0", 10)
|
||||
const indexB = parseInt(b.title.match(/\d+$/)?.[0] ?? "0", 10)
|
||||
return indexA - indexB
|
||||
}
|
||||
return a.timestamp - b.timestamp
|
||||
})
|
||||
|
||||
return { sections, totalDuration }
|
||||
}
|
||||
|
|
@ -1,83 +0,0 @@
|
|||
"use client"
|
||||
|
||||
import { useState, useCallback, type ChangeEvent } from "react"
|
||||
import { Search, Loader2, CheckCircle } from "lucide-react"
|
||||
import { useRouter } from "next/navigation"
|
||||
|
||||
interface TraceSearchProps {
|
||||
initialTraceId?: string
|
||||
isLoading?: boolean
|
||||
hasResults?: boolean
|
||||
}
|
||||
|
||||
export function TraceSearch({ initialTraceId = "", isLoading = false, hasResults = false }: TraceSearchProps) {
|
||||
const [traceId, setTraceId] = useState(initialTraceId)
|
||||
const router = useRouter()
|
||||
|
||||
const handleChange = useCallback((e: ChangeEvent<HTMLInputElement>) => {
|
||||
setTraceId(e.target.value)
|
||||
}, [])
|
||||
|
||||
const handleSearch = useCallback(() => {
|
||||
const trimmedId = traceId.trim()
|
||||
if (!trimmedId) return
|
||||
|
||||
const params = new URLSearchParams(window.location.search)
|
||||
params.set("trace_id", trimmedId)
|
||||
router.push(`/observability?${params.toString()}`)
|
||||
}, [traceId, router])
|
||||
|
||||
const handleKeyDown = useCallback(
|
||||
(e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
if (e.key === "Enter") {
|
||||
handleSearch()
|
||||
}
|
||||
},
|
||||
[handleSearch],
|
||||
)
|
||||
|
||||
const inputBorderClass = hasResults
|
||||
? "border-green-500 dark:border-green-500 focus:ring-green-500"
|
||||
: "border-zinc-300 dark:border-zinc-600 focus:ring-blue-500"
|
||||
|
||||
return (
|
||||
<div className="w-full max-w-2xl mx-auto">
|
||||
<div className="relative flex items-center gap-3">
|
||||
<div className="relative flex-1">
|
||||
<input
|
||||
type="text"
|
||||
value={traceId}
|
||||
onChange={handleChange}
|
||||
onKeyDown={handleKeyDown}
|
||||
placeholder="Enter trace ID..."
|
||||
className={`w-full px-4 py-3 pl-11 ${hasResults ? "pr-11" : ""} text-base rounded-md border ${inputBorderClass} bg-white dark:bg-zinc-950 text-zinc-900 dark:text-zinc-50 placeholder-zinc-400 dark:placeholder-zinc-500 focus:outline-none focus:ring-2 focus:border-transparent transition-all duration-150`}
|
||||
aria-label="Trace ID"
|
||||
/>
|
||||
<Search className="absolute left-4 top-1/2 -translate-y-1/2 h-4 w-4 text-zinc-400" />
|
||||
{hasResults && (
|
||||
<CheckCircle className="absolute right-4 top-1/2 -translate-y-1/2 h-5 w-5 text-green-500" />
|
||||
)}
|
||||
</div>
|
||||
<button
|
||||
onClick={handleSearch}
|
||||
disabled={!traceId.trim() || isLoading}
|
||||
className="px-6 py-3 bg-blue-600 hover:bg-blue-700 disabled:bg-blue-400 disabled:cursor-not-allowed text-white font-medium rounded-md transition-colors duration-150 flex items-center gap-2 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2"
|
||||
>
|
||||
{isLoading ? (
|
||||
<>
|
||||
<Loader2 className="h-5 w-5 animate-spin" />
|
||||
Loading
|
||||
</>
|
||||
) : (
|
||||
"Search"
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
{!hasResults && (
|
||||
<p className="mt-2 text-sm text-zinc-500 dark:text-zinc-400">
|
||||
Paste or type a trace ID to view all associated LLM calls, candidates, and errors
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -1,124 +0,0 @@
|
|||
import {
|
||||
CheckCircle,
|
||||
XCircle,
|
||||
AlertCircle,
|
||||
Timer,
|
||||
DollarSign,
|
||||
Github,
|
||||
Terminal,
|
||||
Hash,
|
||||
Code as CodeIcon,
|
||||
} from "lucide-react"
|
||||
import { InfoIcon } from "./info-icon"
|
||||
|
||||
interface TraceSummaryProps {
|
||||
status: "Completed" | "Partial" | "Failed"
|
||||
source: string
|
||||
durationSeconds: number
|
||||
totalCost: number
|
||||
totalTokens: number
|
||||
candidatesCount?: number
|
||||
}
|
||||
|
||||
export function TraceSummary({
|
||||
status,
|
||||
source,
|
||||
durationSeconds,
|
||||
totalCost,
|
||||
totalTokens,
|
||||
candidatesCount,
|
||||
}: TraceSummaryProps) {
|
||||
let statusColor: string
|
||||
if (status === "Failed") {
|
||||
statusColor = "text-red-600 dark:text-red-400"
|
||||
} else if (status === "Partial") {
|
||||
statusColor = "text-yellow-600 dark:text-yellow-400"
|
||||
} else {
|
||||
statusColor = "text-green-600 dark:text-green-400"
|
||||
}
|
||||
|
||||
let StatusIcon
|
||||
if (status === "Completed") {
|
||||
StatusIcon = CheckCircle
|
||||
} else if (status === "Failed") {
|
||||
StatusIcon = XCircle
|
||||
} else {
|
||||
StatusIcon = AlertCircle
|
||||
}
|
||||
|
||||
const SourceIcon = source.toLowerCase().includes("github") ? Github : Terminal
|
||||
|
||||
return (
|
||||
<div className="bg-white dark:bg-zinc-950 rounded-sm p-6 border border-zinc-200 dark:border-zinc-800">
|
||||
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-6 gap-6">
|
||||
<div className="group">
|
||||
<div className="flex items-center gap-1.5 text-sm text-zinc-600 dark:text-zinc-400 font-medium mb-2">
|
||||
<StatusIcon className="h-4 w-4" />
|
||||
<span>Status</span>
|
||||
<InfoIcon content="Overall trace status based on all contained calls" side="top" />
|
||||
</div>
|
||||
<div className={`text-xl font-bold ${statusColor}`}>{status}</div>
|
||||
</div>
|
||||
|
||||
<div className="group">
|
||||
<div className="flex items-center gap-1.5 text-sm text-zinc-600 dark:text-zinc-400 font-medium mb-2">
|
||||
<SourceIcon className="h-4 w-4" />
|
||||
<span>Source</span>
|
||||
<InfoIcon content="Where this optimization was triggered from" side="top" />
|
||||
</div>
|
||||
<div className="text-xl font-bold text-zinc-900 dark:text-zinc-50">
|
||||
<span className="px-2 py-1 bg-zinc-100 dark:bg-zinc-700 text-zinc-700 dark:text-zinc-300 rounded-sm text-sm hover:bg-zinc-200 dark:hover:bg-zinc-800 transition-colors duration-150">
|
||||
{source}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="group">
|
||||
<div className="flex items-center gap-1.5 text-sm text-zinc-600 dark:text-zinc-400 font-medium mb-2">
|
||||
<Timer className="h-4 w-4" />
|
||||
<span>Duration</span>
|
||||
<InfoIcon content="Total time from first call to last call completion" side="top" />
|
||||
</div>
|
||||
<div className="text-xl font-bold text-zinc-900 dark:text-zinc-50">
|
||||
{durationSeconds.toFixed(2)}s
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="group">
|
||||
<div className="flex items-center gap-1.5 text-sm text-zinc-600 dark:text-zinc-400 font-medium mb-2">
|
||||
<DollarSign className="h-4 w-4" />
|
||||
<span>Cost</span>
|
||||
<InfoIcon content="Sum of all LLM call costs in this trace" side="top" />
|
||||
</div>
|
||||
<div className="text-xl font-bold text-zinc-900 dark:text-zinc-50">
|
||||
${totalCost.toFixed(4)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="group">
|
||||
<div className="flex items-center gap-1.5 text-sm text-zinc-600 dark:text-zinc-400 font-medium mb-2">
|
||||
<Hash className="h-4 w-4" />
|
||||
<span>Tokens</span>
|
||||
<InfoIcon content="Total tokens (prompt + completion) across all calls" side="top" />
|
||||
</div>
|
||||
<div className="text-xl font-bold text-zinc-900 dark:text-zinc-50">
|
||||
{totalTokens.toLocaleString()}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{candidatesCount !== undefined && (
|
||||
<div className="group">
|
||||
<div className="flex items-center gap-1.5 text-sm text-zinc-600 dark:text-zinc-400 font-medium mb-2">
|
||||
<CodeIcon className="h-4 w-4" />
|
||||
<span>Candidates</span>
|
||||
<InfoIcon content="Number of optimization candidates generated" side="top" />
|
||||
</div>
|
||||
<div className="text-xl font-bold text-zinc-900 dark:text-zinc-50">
|
||||
{candidatesCount}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -1,16 +0,0 @@
|
|||
/**
|
||||
* Determines the source of an optimization based on event_type
|
||||
*/
|
||||
export function getTraceSource(eventType: string | null): string {
|
||||
if (!eventType) return "Unknown"
|
||||
|
||||
if (eventType === "pr_created" || eventType === "pr_merged" || eventType === "pr_closed") {
|
||||
return "GitHub Action"
|
||||
}
|
||||
|
||||
if (eventType === "no-pr") {
|
||||
return "CLI/VSCode"
|
||||
}
|
||||
|
||||
return eventType
|
||||
}
|
||||
928
js/cf-webapp/src/components/trace/monaco-diff-viewer.tsx
Normal file
928
js/cf-webapp/src/components/trace/monaco-diff-viewer.tsx
Normal file
|
|
@ -0,0 +1,928 @@
|
|||
"use client"
|
||||
|
||||
import React, { useState, useEffect, useMemo, useCallback, useRef } from "react"
|
||||
import { DiffEditor, useMonaco, DiffOnMount } from "@monaco-editor/react"
|
||||
import {
|
||||
CheckCircle2,
|
||||
XCircle,
|
||||
GitPullRequest,
|
||||
Zap,
|
||||
TestTube,
|
||||
ChevronDown,
|
||||
ChevronUp,
|
||||
ExternalLink,
|
||||
FileCode,
|
||||
Edit3,
|
||||
Save,
|
||||
X,
|
||||
Lock,
|
||||
Monitor,
|
||||
Smartphone,
|
||||
BarChart3,
|
||||
} from "lucide-react"
|
||||
// Ensure you have lucide-react installed as per your package.json
|
||||
import { Loader2, FileText, AlertTriangle } from "lucide-react"
|
||||
import type { ExperimentMetadata, DiffContents } from "@/lib/types" // Adjust path if needed
|
||||
import { getMonacoLanguage } from "@/lib/utils"
|
||||
import ReactMarkdown from "react-markdown"
|
||||
import remarkGfm from "remark-gfm"
|
||||
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter"
|
||||
import { vscDarkPlus } from "react-syntax-highlighter/dist/esm/styles/prism"
|
||||
|
||||
interface MonacoDiffViewerProps {
|
||||
metadata: ExperimentMetadata | null // Full metadata object
|
||||
repoFullName: string // Formatted as "owner/repo"
|
||||
traceId: string
|
||||
review_quality: string
|
||||
review_explanation: string
|
||||
}
|
||||
|
||||
const MonacoDiffViewer: React.FC<MonacoDiffViewerProps> = ({
|
||||
metadata,
|
||||
repoFullName,
|
||||
traceId,
|
||||
review_quality,
|
||||
review_explanation,
|
||||
}) => {
|
||||
const monaco = useMonaco()
|
||||
const [activeFileKey, setActiveFileKey] = useState<string | null>(null)
|
||||
const [showTestDetails, setShowTestDetails] = useState(false)
|
||||
const [showGeneratedTests, setShowGeneratedTests] = useState(false)
|
||||
const [showOptimizationQuality, setShowOptimizationQuality] = useState(false)
|
||||
const [showOptimizationExplanation, setShowOptimizationExplanation] = useState(false)
|
||||
const [isEditing, setIsEditing] = useState(false)
|
||||
const [editSecret, setEditSecret] = useState("")
|
||||
const [showSecretPrompt, setShowSecretPrompt] = useState(false)
|
||||
const [currentEdit, setCurrentEdit] = useState<{ [key: string]: string }>({})
|
||||
const [saveStatus, setSaveStatus] = useState<"idle" | "saving" | "saved" | "error">("idle")
|
||||
const [savedChanges, setSavedChanges] = useState<{ [key: string]: string }>({})
|
||||
const [isMobile, setIsMobile] = useState(false)
|
||||
const [useInlineView, setUseInlineView] = useState(false)
|
||||
|
||||
const isEditingRef = useRef(isEditing)
|
||||
const activeFileKeyRef = useRef(activeFileKey)
|
||||
|
||||
useEffect(() => {
|
||||
isEditingRef.current = isEditing
|
||||
}, [isEditing])
|
||||
|
||||
useEffect(() => {
|
||||
activeFileKeyRef.current = activeFileKey
|
||||
}, [activeFileKey])
|
||||
|
||||
const handleEditorOnMount: DiffOnMount = useCallback(editor => {
|
||||
// Always set up the change listener, but only update state when editing
|
||||
const modifiedEditor = editor.getModifiedEditor()
|
||||
modifiedEditor.onDidChangeModelContent(() => {
|
||||
if (isEditingRef.current) {
|
||||
const value = modifiedEditor.getValue()
|
||||
if (activeFileKeyRef.current && value !== undefined) {
|
||||
setCurrentEdit(prev => ({
|
||||
...prev,
|
||||
[activeFileKeyRef.current!]: value,
|
||||
}))
|
||||
}
|
||||
}
|
||||
})
|
||||
}, [])
|
||||
|
||||
// Merge metadata with saved changes for display
|
||||
const diffContents: DiffContents | null = useMemo(() => {
|
||||
if (!metadata?.diffContents) return null
|
||||
|
||||
const updatedContents = { ...metadata.diffContents }
|
||||
Object.entries(savedChanges).forEach(([fileKey, newContent]) => {
|
||||
if (updatedContents[fileKey]) {
|
||||
updatedContents[fileKey] = {
|
||||
...updatedContents[fileKey],
|
||||
newContent,
|
||||
}
|
||||
}
|
||||
})
|
||||
return updatedContents
|
||||
}, [metadata?.diffContents, savedChanges])
|
||||
const prCommentFields = metadata?.prCommentFields
|
||||
const fileKeys = useMemo(() => {
|
||||
return diffContents ? Object.keys(diffContents) : []
|
||||
}, [diffContents])
|
||||
|
||||
// Calculate test statistics
|
||||
const testStats = useMemo(() => {
|
||||
const stats = {
|
||||
totalPassed: 0,
|
||||
totalFailed: 0,
|
||||
categories: [] as { name: string; passed: number; failed: number; icon: string }[],
|
||||
}
|
||||
|
||||
if (prCommentFields?.report_table) {
|
||||
Object.entries(prCommentFields.report_table).forEach(([category, results]) => {
|
||||
stats.totalPassed += results.passed
|
||||
stats.totalFailed += results.failed
|
||||
|
||||
// Map category names to icons
|
||||
let icon = "🧪"
|
||||
if (category.includes("Replay")) icon = "⏪"
|
||||
else if (category.includes("Unit")) icon = "⚙️"
|
||||
else if (category.includes("Coverage")) icon = "🔎"
|
||||
else if (category.includes("Regression")) icon = "🌀"
|
||||
else if (category.includes("Inspired")) icon = "🎨"
|
||||
|
||||
stats.categories.push({
|
||||
name: category,
|
||||
passed: results.passed,
|
||||
failed: results.failed,
|
||||
icon,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
return stats
|
||||
}, [prCommentFields])
|
||||
|
||||
const handleEditClick = () => {
|
||||
setShowSecretPrompt(true)
|
||||
}
|
||||
|
||||
const handleSecretSubmit = () => {
|
||||
if (editSecret === "codeflash-edit-2025") {
|
||||
setIsEditing(true)
|
||||
setShowSecretPrompt(false)
|
||||
// Initialize current edit with current content if not already set
|
||||
if (activeFileKey && diffContents?.[activeFileKey] && !currentEdit[activeFileKey]) {
|
||||
setCurrentEdit(prev => ({
|
||||
...prev,
|
||||
[activeFileKey]:
|
||||
diffContents[activeFileKey].newContent || diffContents[activeFileKey].oldContent || "",
|
||||
}))
|
||||
}
|
||||
} else {
|
||||
alert("Invalid secret!")
|
||||
}
|
||||
}
|
||||
|
||||
const handleSaveCode = async () => {
|
||||
if (!activeFileKey || !currentEdit[activeFileKey]) return
|
||||
|
||||
setSaveStatus("saving")
|
||||
try {
|
||||
const response = await fetch(`/api/traces/${traceId}/save-modified-code`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
fileKey: activeFileKey,
|
||||
modifiedCode: currentEdit[activeFileKey],
|
||||
secret: editSecret,
|
||||
}),
|
||||
})
|
||||
|
||||
if (response.ok) {
|
||||
setSaveStatus("saved")
|
||||
|
||||
// Update local saved changes state to show the changes immediately
|
||||
setSavedChanges(prev => ({
|
||||
...prev,
|
||||
[activeFileKey]: currentEdit[activeFileKey],
|
||||
}))
|
||||
|
||||
// Clear current edit and auto-return to diff view after successful save
|
||||
setTimeout(() => {
|
||||
setSaveStatus("idle")
|
||||
setIsEditing(false)
|
||||
setCurrentEdit(prev => {
|
||||
const newEdit = { ...prev }
|
||||
delete newEdit[activeFileKey!]
|
||||
return newEdit
|
||||
})
|
||||
}, 1500)
|
||||
} else {
|
||||
setSaveStatus("error")
|
||||
setTimeout(() => setSaveStatus("idle"), 3000)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to save modified code:", error)
|
||||
setSaveStatus("error")
|
||||
setTimeout(() => setSaveStatus("idle"), 3000)
|
||||
}
|
||||
}
|
||||
|
||||
const handleCancelEdit = () => {
|
||||
setIsEditing(false)
|
||||
setEditSecret("")
|
||||
// Revert changes for current file
|
||||
if (activeFileKey) {
|
||||
setCurrentEdit(prev => {
|
||||
const newEdit = { ...prev }
|
||||
delete newEdit[activeFileKey]
|
||||
return newEdit
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (fileKeys.length > 0 && !activeFileKey) {
|
||||
setActiveFileKey(fileKeys[0])
|
||||
}
|
||||
}, [fileKeys, activeFileKey])
|
||||
|
||||
// Mobile detection and responsive handler
|
||||
useEffect(() => {
|
||||
const checkMobile = () => {
|
||||
const mobile = window.innerWidth < 768
|
||||
setIsMobile(mobile)
|
||||
setUseInlineView(mobile)
|
||||
}
|
||||
|
||||
checkMobile()
|
||||
window.addEventListener("resize", checkMobile)
|
||||
return () => window.removeEventListener("resize", checkMobile)
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (monaco) {
|
||||
// Define your custom dark theme for Monaco Editor
|
||||
monaco.editor.defineTheme("codeflash-python-dark", {
|
||||
base: "vs-dark",
|
||||
inherit: true,
|
||||
rules: [
|
||||
{ token: "comment.python", foreground: "6A9955" }, // Python comments
|
||||
{ token: "keyword.python", foreground: "569CD6" }, // Python keywords
|
||||
{ token: "string.python", foreground: "CE9178" }, // Python strings
|
||||
{ token: "number.python", foreground: "B5CEA8" }, // Python numbers
|
||||
{ token: "identifier.python", foreground: "9CDCFE" },
|
||||
{ token: "type.identifier.python", foreground: "4EC9B0" }, // class names, etc.
|
||||
],
|
||||
colors: {
|
||||
"editor.background": "#0A0E14",
|
||||
"editor.foreground": "#F8F8F2",
|
||||
"editorLineNumber.foreground": "#6272A4",
|
||||
"editor.selectionBackground": "#44475A",
|
||||
"editor.lineHighlightBackground": "#1A1F29",
|
||||
"diffEditor.insertedTextBackground": "#50FA7B33",
|
||||
"diffEditor.removedTextBackground": "#FF555533",
|
||||
"diffEditor.insertedLineBackground": "#50FA7B22",
|
||||
"diffEditor.removedLineBackground": "#FF555522",
|
||||
"diffEditorGutter.insertedLineBackground": "#50FA7B",
|
||||
"diffEditorGutter.removedLineBackground": "#FF5555",
|
||||
},
|
||||
})
|
||||
}
|
||||
}, [monaco])
|
||||
|
||||
// Loading or error states - NOW AFTER ALL HOOKS
|
||||
if (!metadata) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center h-screen bg-[#0f0f0f] text-slate-400">
|
||||
<Loader2 className="h-12 w-12 animate-spin text-sky-500 mb-4" />
|
||||
<p className="text-lg">
|
||||
Loading trace details for <span className="font-mono">{traceId}</span>...
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (!diffContents || fileKeys.length === 0) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center h-screen bg-[#0f0f0f] text-slate-400">
|
||||
<AlertTriangle className="h-12 w-12 text-amber-500 mb-4" />
|
||||
<p className="text-lg">No diff content available for this trace.</p>
|
||||
<p className="text-sm font-mono mt-2">Trace ID: {traceId}</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const currentDiff = activeFileKey && diffContents ? diffContents[activeFileKey] : null
|
||||
const functionName = prCommentFields?.function_name || "N/A"
|
||||
const speedup = prCommentFields?.speedup_pct || prCommentFields?.speedup_x || "N/A"
|
||||
const explanation = prCommentFields?.optimization_explanation
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-screen bg-gradient-to-br from-slate-900 via-slate-800 to-slate-900 text-slate-200">
|
||||
{/* Header Section - Mobile Optimized */}
|
||||
<div className="px-3 sm:px-4 md:px-6 py-2 sm:py-3 md:py-4 border-b border-slate-700/50 overflow-y-auto max-h-[40vh] md:max-h-none">
|
||||
<div className="flex flex-col gap-3 sm:gap-4 md:gap-6">
|
||||
{/* Top Row - Title with PR Link and Observability Link */}
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<div className="flex items-center gap-2 sm:gap-3 min-w-0 flex-1">
|
||||
<h1 className="text-lg sm:text-xl md:text-2xl lg:text-3xl font-bold bg-gradient-to-r from-cyan-400 via-blue-500 to-purple-600 text-transparent bg-clip-text truncate">
|
||||
CodeFlash Optimization
|
||||
</h1>
|
||||
{metadata.pullNumber && (
|
||||
<a
|
||||
href={`https://github.com/${metadata.owner || repoFullName.split("/")[0]}/${metadata.repo || repoFullName.split("/")[1]}/pull/${metadata.pullNumber}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center gap-1 text-cyan-400 hover:text-cyan-300 transition-colors bg-slate-800/50 px-2 py-1 rounded-md text-xs sm:text-sm flex-shrink-0"
|
||||
>
|
||||
<GitPullRequest className="h-3 w-3 sm:h-4 sm:w-4" />
|
||||
<span className="hidden sm:inline">PR #{metadata.pullNumber}</span>
|
||||
<span className="sm:hidden">#{metadata.pullNumber}</span>
|
||||
<ExternalLink className="h-2.5 w-2.5 sm:h-3 sm:w-3" />
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
{/* Observability Link */}
|
||||
<a
|
||||
href={`/observability/trace/${traceId}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center gap-1 text-cyan-400 hover:text-cyan-300 transition-colors bg-slate-800/50 px-2 py-1 rounded-md text-xs sm:text-sm flex-shrink-0"
|
||||
>
|
||||
<BarChart3 className="h-3 w-3 sm:h-4 sm:w-4" />
|
||||
<span className="hidden sm:inline">Observability</span>
|
||||
<span className="sm:hidden">Obs</span>
|
||||
<ExternalLink className="h-2.5 w-2.5 sm:h-3 sm:w-3" />
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{/* Info Row - Repository and Function (Compact on Mobile) */}
|
||||
<div className="flex flex-col sm:flex-row sm:items-center gap-2 sm:gap-4 text-xs sm:text-sm">
|
||||
<div className="flex items-center gap-2 min-w-0">
|
||||
<FileCode className="h-3 w-3 sm:h-4 sm:w-4 text-cyan-400 flex-shrink-0" />
|
||||
<span className="text-slate-400 flex-shrink-0">Repo:</span>
|
||||
<span className="text-slate-200 font-medium truncate">{repoFullName}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 min-w-0">
|
||||
<span className="text-slate-400 flex-shrink-0">Function:</span>
|
||||
<code className="text-purple-400 bg-slate-800/50 px-1.5 sm:px-2 py-0.5 sm:py-1 rounded font-mono text-xs sm:text-sm truncate">
|
||||
{functionName}
|
||||
</code>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Performance Metrics Row - Compact on Mobile */}
|
||||
<div className="flex flex-wrap items-center gap-2 sm:gap-4">
|
||||
{/* Performance Boost - Smaller on Mobile */}
|
||||
<div className="bg-slate-800/30 rounded-lg p-2 sm:p-3 md:p-4 border border-slate-700/30 flex items-center gap-1.5 sm:gap-2">
|
||||
<Zap className="h-4 w-4 sm:h-5 sm:w-5 md:h-6 md:w-6 text-yellow-400 flex-shrink-0" />
|
||||
<div>
|
||||
<div className="text-[10px] sm:text-xs text-slate-400 uppercase tracking-wider">
|
||||
Boost
|
||||
</div>
|
||||
<span className="text-lg sm:text-xl md:text-2xl lg:text-3xl font-bold bg-gradient-to-r from-green-400 to-emerald-500 text-transparent bg-clip-text block leading-tight">
|
||||
{speedup}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Additional Metrics - Hidden on very small screens */}
|
||||
{prCommentFields?.loop_count && (
|
||||
<div className="hidden sm:block text-center">
|
||||
<div className="text-[10px] sm:text-xs text-slate-400 uppercase tracking-wider mb-1">
|
||||
Loops
|
||||
</div>
|
||||
<div className="text-sm sm:text-base md:text-lg font-semibold text-blue-400">
|
||||
{prCommentFields.loop_count.toLocaleString()}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{prCommentFields?.original_runtime && prCommentFields?.best_runtime && (
|
||||
<div className="hidden md:block text-center">
|
||||
<div className="text-[10px] sm:text-xs text-slate-400 uppercase tracking-wider mb-1">
|
||||
Runtime
|
||||
</div>
|
||||
<div className="text-xs sm:text-sm">
|
||||
<span className="text-red-400 line-through">
|
||||
{prCommentFields.original_runtime}
|
||||
</span>
|
||||
<span className="mx-1 sm:mx-2 text-slate-500">→</span>
|
||||
<span className="text-green-400">{prCommentFields.best_runtime}</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Edit Button - Compact on Mobile */}
|
||||
<div className="ml-auto flex items-center gap-1.5 sm:gap-2">
|
||||
{!isEditing ? (
|
||||
<button
|
||||
onClick={handleEditClick}
|
||||
className="flex items-center gap-1 sm:gap-2 px-2 sm:px-3 py-1.5 sm:py-2 bg-purple-600 hover:bg-purple-700 text-white rounded-lg transition-colors text-xs sm:text-sm font-medium"
|
||||
>
|
||||
<Edit3 className="h-3 w-3 sm:h-4 sm:w-4" />
|
||||
<span className="hidden sm:inline">Edit Code</span>
|
||||
<span className="sm:hidden">Edit</span>
|
||||
</button>
|
||||
) : (
|
||||
<div className="flex items-center gap-1.5 sm:gap-2">
|
||||
<button
|
||||
onClick={handleSaveCode}
|
||||
disabled={saveStatus === "saving"}
|
||||
className="flex items-center gap-1 sm:gap-2 px-2 sm:px-3 py-1.5 sm:py-2 bg-green-600 hover:bg-green-700 disabled:bg-green-800 text-white rounded-lg transition-colors text-xs sm:text-sm font-medium"
|
||||
>
|
||||
<Save className="h-3 w-3 sm:h-4 sm:w-4" />
|
||||
<span className="hidden sm:inline">
|
||||
{saveStatus === "saving"
|
||||
? "Saving..."
|
||||
: saveStatus === "saved"
|
||||
? "Saved!"
|
||||
: "Save"}
|
||||
</span>
|
||||
<span className="sm:hidden">
|
||||
{saveStatus === "saving" ? "..." : saveStatus === "saved" ? "✓" : "Save"}
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={handleCancelEdit}
|
||||
className="flex items-center gap-1 sm:gap-2 px-2 sm:px-3 py-1.5 sm:py-2 bg-red-600 hover:bg-red-700 text-white rounded-lg transition-colors text-xs sm:text-sm font-medium"
|
||||
>
|
||||
<X className="h-3 w-3 sm:h-4 sm:w-4" />
|
||||
<span className="hidden sm:inline">Cancel</span>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Test Results Summary - Collapsed by default on mobile */}
|
||||
{testStats.totalPassed > 0 || testStats.totalFailed > 0 ? (
|
||||
<div className="bg-slate-800/30 rounded-lg p-2 sm:p-3 md:p-4 border border-slate-700/30">
|
||||
<div
|
||||
className="flex items-center justify-between cursor-pointer gap-2"
|
||||
onClick={() => setShowTestDetails(!showTestDetails)}
|
||||
>
|
||||
<div className="flex items-center gap-2 sm:gap-3 min-w-0 flex-1">
|
||||
<TestTube className="h-4 w-4 sm:h-5 sm:w-5 text-blue-400 flex-shrink-0" />
|
||||
<span className="font-semibold text-xs sm:text-sm md:text-base truncate">
|
||||
Test Results
|
||||
</span>
|
||||
<div className="flex items-center gap-2 sm:gap-4 flex-shrink-0">
|
||||
<div className="flex items-center gap-1">
|
||||
<CheckCircle2 className="h-3 w-3 sm:h-4 sm:w-4 text-green-400" />
|
||||
<span className="text-green-400 font-medium text-xs sm:text-sm">
|
||||
{testStats.totalPassed}
|
||||
</span>
|
||||
</div>
|
||||
{testStats.totalFailed > 0 && (
|
||||
<div className="flex items-center gap-1">
|
||||
<XCircle className="h-3 w-3 sm:h-4 sm:w-4 text-red-400" />
|
||||
<span className="text-red-400 font-medium text-xs sm:text-sm">
|
||||
{testStats.totalFailed}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{showTestDetails ? (
|
||||
<ChevronUp className="h-4 w-4 flex-shrink-0" />
|
||||
) : (
|
||||
<ChevronDown className="h-4 w-4 flex-shrink-0" />
|
||||
)}
|
||||
</div>
|
||||
|
||||
{showTestDetails && (
|
||||
<div className="mt-3 sm:mt-4 grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-2 sm:gap-3">
|
||||
{testStats.categories.map((category, idx) => (
|
||||
<div
|
||||
key={idx}
|
||||
className="bg-slate-700/50 rounded-lg p-2 sm:p-3 flex items-center justify-between gap-2"
|
||||
>
|
||||
<div className="flex items-center gap-2 min-w-0 flex-1">
|
||||
<span className="text-xs sm:text-sm text-slate-300 truncate">
|
||||
{category.name}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-xs sm:text-sm flex-shrink-0">
|
||||
{category.passed > 0 && (
|
||||
<span className="text-green-400">{category.passed}✓</span>
|
||||
)}
|
||||
{category.failed > 0 && (
|
||||
<span className="text-red-400">{category.failed}✗</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* File Path/Tabs - Mobile Optimized */}
|
||||
{fileKeys.length === 1 ? (
|
||||
// Single file - show full path with view toggle
|
||||
<div className="bg-[rgba(15,15,15,0.95)] border-b border-[rgba(255,255,255,0.05)] px-3 sm:px-4 md:px-5 py-2 sm:py-2.5 md:py-3 flex items-center justify-between gap-2">
|
||||
<div className="flex items-center gap-1.5 sm:gap-2 min-w-0 flex-1">
|
||||
<FileText size={12} className="text-sky-400 flex-shrink-0 sm:w-3.5 sm:h-3.5" />
|
||||
<span className="text-xs sm:text-sm text-slate-300 font-mono truncate">
|
||||
{fileKeys[0]}
|
||||
</span>
|
||||
</div>
|
||||
{/* View Toggle for mobile compatibility */}
|
||||
<div className="flex items-center gap-1.5 sm:gap-2 flex-shrink-0">
|
||||
<button
|
||||
onClick={() => setUseInlineView(!useInlineView)}
|
||||
className={`flex items-center gap-1 sm:gap-1.5 px-2 sm:px-3 py-1 rounded-md text-[10px] sm:text-xs transition-colors ${
|
||||
useInlineView
|
||||
? "bg-blue-600/20 text-blue-400 border border-blue-600/30"
|
||||
: "bg-slate-700/50 text-slate-400 hover:text-slate-300"
|
||||
}`}
|
||||
title={useInlineView ? "Switch to side-by-side view" : "Switch to inline view"}
|
||||
>
|
||||
{useInlineView ? (
|
||||
<Smartphone size={10} className="sm:w-3 sm:h-3" />
|
||||
) : (
|
||||
<Monitor size={10} className="sm:w-3 sm:h-3" />
|
||||
)}
|
||||
<span className="hidden sm:inline">{useInlineView ? "Inline" : "Side-by-side"}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
// Multiple files - show tabs with full path on hover
|
||||
<div className="bg-[rgba(15,15,15,0.95)] border-b border-[rgba(255,255,255,0.05)]">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<div className="flex overflow-x-auto whitespace-nowrap scrollbar-thin scrollbar-thumb-slate-700 scrollbar-track-slate-800 flex-1 min-w-0">
|
||||
{fileKeys.map(fileKey => (
|
||||
<button
|
||||
key={fileKey}
|
||||
onClick={() => setActiveFileKey(fileKey)}
|
||||
title={fileKey}
|
||||
className={`px-3 sm:px-4 md:px-5 py-2 sm:py-2.5 md:py-3 text-xs sm:text-sm transition-all duration-200 ease-in-out focus:outline-none flex items-center gap-1.5 sm:gap-2 flex-shrink-0
|
||||
${
|
||||
activeFileKey === fileKey
|
||||
? "text-white bg-[rgba(255,255,255,0.05)] border-b-2 border-sky-400"
|
||||
: "text-slate-400 hover:text-slate-200 hover:bg-[rgba(255,255,255,0.03)] border-b-2 border-transparent"
|
||||
}`}
|
||||
>
|
||||
<FileText
|
||||
size={12}
|
||||
className={`${activeFileKey === fileKey ? "text-sky-400" : "text-slate-500"} flex-shrink-0 sm:w-3.5 sm:h-3.5`}
|
||||
/>
|
||||
<span className="truncate max-w-[120px] sm:max-w-none">
|
||||
{fileKey.split("/").pop()}
|
||||
</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
{/* View Toggle for mobile compatibility */}
|
||||
<div className="flex items-center gap-1.5 sm:gap-2 px-2 sm:px-3 md:px-5 flex-shrink-0">
|
||||
<button
|
||||
onClick={() => setUseInlineView(!useInlineView)}
|
||||
className={`flex items-center gap-1 sm:gap-1.5 px-2 sm:px-3 py-1 rounded-md text-[10px] sm:text-xs transition-colors ${
|
||||
useInlineView
|
||||
? "bg-blue-600/20 text-blue-400 border border-blue-600/30"
|
||||
: "bg-slate-700/50 text-slate-400 hover:text-slate-300"
|
||||
}`}
|
||||
title={useInlineView ? "Switch to side-by-side view" : "Switch to inline view"}
|
||||
>
|
||||
{useInlineView ? (
|
||||
<Smartphone size={10} className="sm:w-3 sm:h-3" />
|
||||
) : (
|
||||
<Monitor size={10} className="sm:w-3 sm:h-3" />
|
||||
)}
|
||||
<span className="hidden sm:inline">
|
||||
{useInlineView ? "Inline" : "Side-by-side"}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Monaco Editor Container */}
|
||||
<div className="flex-grow relative min-h-0">
|
||||
{activeFileKey && currentDiff && monaco ? (
|
||||
// Check for empty diff scenarios first (only in non-editing mode)
|
||||
!isEditing &&
|
||||
(currentDiff.oldContent || "") === (currentDiff.newContent || "") &&
|
||||
(currentDiff.oldContent || "").trim() === "" ? (
|
||||
// Both contents are empty
|
||||
<div className="absolute inset-0 flex flex-col items-center justify-center bg-[#0A0A0A] text-slate-400">
|
||||
<FileText className="h-12 w-12 text-slate-500 mb-4" />
|
||||
<h3 className="text-lg font-medium text-slate-200 mb-2">No Changes to Display</h3>
|
||||
<p className="text-sm text-slate-400 max-w-md text-center px-4">
|
||||
The file content is empty. The staging branch may have been merged or the changes
|
||||
have been reverted.
|
||||
</p>
|
||||
</div>
|
||||
) : !isEditing && (currentDiff.oldContent || "") === (currentDiff.newContent || "") ? (
|
||||
// Contents are identical (no diff)
|
||||
<div className="absolute inset-0 flex flex-col items-center justify-center bg-[#0A0A0A] text-slate-400">
|
||||
<FileText className="h-12 w-12 text-slate-500 mb-4" />
|
||||
<h3 className="text-lg font-medium text-slate-200 mb-2">No Differences Found</h3>
|
||||
<p className="text-sm text-slate-400 max-w-md text-center px-4">
|
||||
The original and optimized code are identical. The changes may have already been
|
||||
applied or reverted.
|
||||
</p>
|
||||
</div>
|
||||
) : isEditing ? (
|
||||
// Edit Mode with Diff View - Like VS Code changes view
|
||||
<DiffEditor
|
||||
height="100%"
|
||||
original={currentDiff.oldContent || ""}
|
||||
modified={currentEdit[activeFileKey] || currentDiff.newContent || ""}
|
||||
language={activeFileKey ? getMonacoLanguage(activeFileKey) : "python"}
|
||||
theme="codeflash-python-dark"
|
||||
onMount={handleEditorOnMount}
|
||||
options={{
|
||||
automaticLayout: true,
|
||||
renderSideBySide: !useInlineView,
|
||||
readOnly: false, // Allow editing on the modified side
|
||||
scrollBeyondLastLine: false,
|
||||
minimap: { enabled: !isMobile, scale: isMobile ? 0.5 : 1, size: "proportional" },
|
||||
diffCodeLens: true,
|
||||
renderIndicators: true,
|
||||
ignoreTrimWhitespace: false,
|
||||
renderWhitespace: "boundary",
|
||||
fontSize: isMobile ? 12 : 13,
|
||||
fontFamily: "'Fira Code', 'Cascadia Code', Consolas, 'Courier New', monospace",
|
||||
fontLigatures: !isMobile, // Disable font ligatures on mobile for better performance
|
||||
wordWrap: isMobile ? "on" : "off", // Enable word wrap on mobile
|
||||
scrollbar: {
|
||||
verticalScrollbarSize: isMobile ? 14 : 10,
|
||||
horizontalScrollbarSize: isMobile ? 14 : 10,
|
||||
useShadows: false,
|
||||
},
|
||||
originalEditable: false, // Original side stays read-only
|
||||
enableSplitViewResizing: !isMobile,
|
||||
// Enable diff review mode for better editing experience
|
||||
diffWordWrap: isMobile ? "on" : "off",
|
||||
// Mobile-specific optimizations
|
||||
mouseWheelZoom: !isMobile,
|
||||
contextmenu: !isMobile,
|
||||
quickSuggestions: !isMobile,
|
||||
parameterHints: { enabled: !isMobile },
|
||||
suggest: !isMobile ? {} : undefined,
|
||||
hover: { enabled: !isMobile },
|
||||
}}
|
||||
loading={
|
||||
<div className="absolute inset-0 flex flex-col items-center justify-center bg-[#0A0A0A] text-slate-400">
|
||||
<Loader2 className="h-10 w-10 animate-spin text-sky-500 mb-3" />
|
||||
<p>Loading Diff Editor...</p>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
// Diff View Mode
|
||||
<DiffEditor
|
||||
height="100%"
|
||||
original={currentDiff.oldContent || ""}
|
||||
modified={currentDiff.newContent || ""}
|
||||
language={activeFileKey ? getMonacoLanguage(activeFileKey) : "python"}
|
||||
theme="codeflash-python-dark"
|
||||
onMount={handleEditorOnMount}
|
||||
options={{
|
||||
automaticLayout: true,
|
||||
renderSideBySide: !useInlineView,
|
||||
readOnly: true,
|
||||
scrollBeyondLastLine: false,
|
||||
minimap: { enabled: !isMobile, scale: isMobile ? 0.5 : 1, size: "proportional" },
|
||||
diffCodeLens: true,
|
||||
renderIndicators: true,
|
||||
ignoreTrimWhitespace: false,
|
||||
renderWhitespace: "boundary",
|
||||
fontSize: isMobile ? 12 : 13,
|
||||
fontFamily: "'Fira Code', 'Cascadia Code', Consolas, 'Courier New', monospace",
|
||||
fontLigatures: !isMobile, // Disable font ligatures on mobile for better performance
|
||||
wordWrap: isMobile ? "on" : "off", // Enable word wrap on mobile
|
||||
scrollbar: {
|
||||
verticalScrollbarSize: isMobile ? 14 : 10,
|
||||
horizontalScrollbarSize: isMobile ? 14 : 10,
|
||||
useShadows: false,
|
||||
},
|
||||
originalEditable: false,
|
||||
enableSplitViewResizing: !isMobile,
|
||||
// Mobile-specific optimizations
|
||||
mouseWheelZoom: !isMobile,
|
||||
contextmenu: !isMobile,
|
||||
quickSuggestions: false,
|
||||
parameterHints: { enabled: false },
|
||||
suggest: undefined,
|
||||
hover: { enabled: !isMobile },
|
||||
}}
|
||||
loading={
|
||||
<div className="absolute inset-0 flex flex-col items-center justify-center bg-[#0A0A0A] text-slate-400">
|
||||
<Loader2 className="h-10 w-10 animate-spin text-sky-500 mb-3" />
|
||||
<p>Loading Diff Editor...</p>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
)
|
||||
) : (
|
||||
<div className="absolute inset-0 flex flex-col items-center justify-center bg-[#0A0A0A] text-slate-500">
|
||||
<Loader2 className="h-10 w-10 animate-spin text-sky-500 mb-3" />
|
||||
<p>{!monaco ? "Initializing Editor Subsystem..." : "Preparing View..."}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{/* Bottom Section with Explanation and Generated Tests - Mobile Optimized */}
|
||||
<div className="bg-gradient-to-t from-slate-900 to-slate-800/50 border-t border-slate-700/50 max-h-[30vh] sm:max-h-[35vh] md:max-h-none overflow-y-auto">
|
||||
{/* Optimization review details */}
|
||||
{review_quality && (
|
||||
<div className="p-2 sm:p-3 md:p-4 border-b border-slate-700/30">
|
||||
<div
|
||||
className="flex items-center justify-between cursor-pointer gap-2"
|
||||
onClick={() => setShowOptimizationQuality(!showOptimizationQuality)}
|
||||
>
|
||||
<h3 className="text-xs sm:text-sm font-semibold text-cyan-400 flex items-center gap-1.5 sm:gap-2 min-w-0 flex-1">
|
||||
<Zap className="h-3 w-3 sm:h-4 sm:w-4 flex-shrink-0" />
|
||||
<span className="truncate">🎯 Quality: {review_quality}</span>
|
||||
</h3>
|
||||
{showOptimizationQuality ? (
|
||||
<ChevronUp className="h-3 w-3 sm:h-4 sm:w-4 text-slate-400 flex-shrink-0" />
|
||||
) : (
|
||||
<ChevronDown className="h-3 w-3 sm:h-4 sm:w-4 text-slate-400 flex-shrink-0" />
|
||||
)}
|
||||
</div>
|
||||
{showOptimizationQuality && (
|
||||
<div className="mt-2 text-xs sm:text-sm text-slate-300 whitespace-pre-wrap bg-slate-800/50 rounded-lg p-2 sm:p-3 max-h-24 sm:max-h-32 overflow-y-auto scrollbar-thin scrollbar-thumb-slate-600 scrollbar-track-slate-800">
|
||||
{review_explanation}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{/* Optimization Explanation */}
|
||||
{explanation && (
|
||||
<div className="p-2 sm:p-3 md:p-4 border-b border-slate-700/30">
|
||||
<div
|
||||
className="flex items-center justify-between cursor-pointer gap-2"
|
||||
onClick={() => setShowOptimizationExplanation(!showOptimizationExplanation)}
|
||||
>
|
||||
<h3 className="text-xs sm:text-sm font-semibold text-cyan-400 flex items-center gap-1.5 sm:gap-2">
|
||||
<Zap className="h-3 w-3 sm:h-4 sm:w-4 flex-shrink-0" />
|
||||
<span className="hidden sm:inline">Optimization Explanation</span>
|
||||
<span className="sm:hidden">Explanation</span>
|
||||
</h3>
|
||||
{showOptimizationExplanation ? (
|
||||
<ChevronUp className="h-3 w-3 sm:h-4 sm:w-4 text-slate-400 flex-shrink-0" />
|
||||
) : (
|
||||
<ChevronDown className="h-3 w-3 sm:h-4 sm:w-4 text-slate-400 flex-shrink-0" />
|
||||
)}
|
||||
</div>
|
||||
{showOptimizationExplanation && (
|
||||
<div className="mt-2 text-xs sm:text-sm text-slate-300 whitespace-pre-wrap bg-slate-800/50 rounded-lg p-2 sm:p-3 max-h-24 sm:max-h-32 overflow-y-auto scrollbar-thin scrollbar-thumb-slate-600 scrollbar-track-slate-800">
|
||||
{explanation}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Generated Tests Toggle */}
|
||||
{metadata.generatedTests && (
|
||||
<div className="p-2 sm:p-3 md:p-4">
|
||||
<button
|
||||
onClick={() => setShowGeneratedTests(!showGeneratedTests)}
|
||||
className="flex items-center gap-1.5 sm:gap-2 text-xs sm:text-sm font-semibold text-purple-400 hover:text-purple-300 transition-colors w-full"
|
||||
>
|
||||
<TestTube className="h-3 w-3 sm:h-4 sm:w-4 flex-shrink-0" />
|
||||
<span className="hidden sm:inline">Generated Tests</span>
|
||||
<span className="sm:hidden">Tests</span>
|
||||
{showGeneratedTests ? (
|
||||
<ChevronUp className="h-3 w-3 sm:h-4 sm:w-4 ml-auto flex-shrink-0" />
|
||||
) : (
|
||||
<ChevronDown className="h-3 w-3 sm:h-4 sm:w-4 ml-auto flex-shrink-0" />
|
||||
)}
|
||||
</button>
|
||||
{showGeneratedTests && (
|
||||
<div className="mt-2 sm:mt-3 bg-slate-800/50 rounded-lg p-2 sm:p-3 max-h-48 sm:max-h-64 overflow-y-auto scrollbar-thin scrollbar-thumb-slate-600 scrollbar-track-slate-800">
|
||||
<ReactMarkdown
|
||||
className="prose prose-sm prose-invert max-w-none"
|
||||
remarkPlugins={[remarkGfm]}
|
||||
components={{
|
||||
pre: ({ children, ...props }) => {
|
||||
return (
|
||||
<div className="not-prose">
|
||||
<pre className="bg-slate-900/50 rounded-md overflow-x-auto" {...props}>
|
||||
{children}
|
||||
</pre>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
code: props => {
|
||||
const { inline, className, children, ...restProps } = props as {
|
||||
inline?: boolean
|
||||
className?: string
|
||||
children: React.ReactNode
|
||||
[key: string]: unknown
|
||||
}
|
||||
const match = /language-(\w+)/.exec(className || "")
|
||||
const language = match ? match[1] : null
|
||||
|
||||
return !inline && language ? (
|
||||
<SyntaxHighlighter
|
||||
style={vscDarkPlus}
|
||||
language={language}
|
||||
PreTag="div"
|
||||
className="!bg-slate-900/50 !text-[10px] sm:!text-xs rounded-md"
|
||||
customStyle={{
|
||||
margin: 0,
|
||||
padding: "0.5rem",
|
||||
backgroundColor: "rgb(15 23 42 / 0.5)",
|
||||
fontSize: "0.625rem",
|
||||
lineHeight: "1.4",
|
||||
}}
|
||||
{...restProps}
|
||||
>
|
||||
{String(children).replace(/\n$/, "")}
|
||||
</SyntaxHighlighter>
|
||||
) : (
|
||||
<code
|
||||
className="bg-slate-700/50 px-1 py-0.5 rounded text-[10px] sm:text-xs text-cyan-300 font-mono"
|
||||
{...restProps}
|
||||
>
|
||||
{children}
|
||||
</code>
|
||||
)
|
||||
},
|
||||
p: ({ children }) => (
|
||||
<p className="text-xs sm:text-sm text-slate-300 mb-2 sm:mb-3">{children}</p>
|
||||
),
|
||||
h1: ({ children }) => (
|
||||
<h1 className="text-base sm:text-lg font-bold text-white mb-1.5 sm:mb-2">
|
||||
{children}
|
||||
</h1>
|
||||
),
|
||||
h2: ({ children }) => (
|
||||
<h2 className="text-sm sm:text-base font-semibold text-white mb-1.5 sm:mb-2">
|
||||
{children}
|
||||
</h2>
|
||||
),
|
||||
h3: ({ children }) => (
|
||||
<h3 className="text-xs sm:text-sm font-semibold text-white mb-1">
|
||||
{children}
|
||||
</h3>
|
||||
),
|
||||
ul: ({ children }) => (
|
||||
<ul className="list-disc list-inside text-xs sm:text-sm text-slate-300 mb-2 sm:mb-3">
|
||||
{children}
|
||||
</ul>
|
||||
),
|
||||
ol: ({ children }) => (
|
||||
<ol className="list-decimal list-inside text-xs sm:text-sm text-slate-300 mb-2 sm:mb-3">
|
||||
{children}
|
||||
</ol>
|
||||
),
|
||||
li: ({ children }) => (
|
||||
<li className="text-xs sm:text-sm text-slate-300 mb-0.5 sm:mb-1">
|
||||
{children}
|
||||
</li>
|
||||
),
|
||||
}}
|
||||
>
|
||||
{(() => {
|
||||
// Check if generatedTests already has markdown code blocks
|
||||
const testsContent = metadata.generatedTests.trim()
|
||||
const hasCodeBlocks = testsContent.includes("```")
|
||||
|
||||
// If it doesn't have code blocks, wrap it as Python code
|
||||
if (!hasCodeBlocks) {
|
||||
return "```python\n" + testsContent + "\n```"
|
||||
}
|
||||
|
||||
// Otherwise, return as-is
|
||||
return testsContent
|
||||
})()}
|
||||
</ReactMarkdown>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Secret Prompt Modal - Mobile Optimized */}
|
||||
{showSecretPrompt && (
|
||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4">
|
||||
<div className="bg-slate-800 rounded-lg p-4 sm:p-6 max-w-md w-full mx-4">
|
||||
<div className="flex items-center gap-2 sm:gap-3 mb-3 sm:mb-4">
|
||||
<Lock className="h-4 w-4 sm:h-5 sm:w-5 text-amber-400 flex-shrink-0" />
|
||||
<h3 className="text-base sm:text-lg font-semibold text-white">Enter Edit Secret</h3>
|
||||
</div>
|
||||
<p className="text-slate-300 text-xs sm:text-sm mb-3 sm:mb-4">
|
||||
Please enter the secret key to enable code editing.
|
||||
</p>
|
||||
<input
|
||||
type="password"
|
||||
placeholder="Enter secret..."
|
||||
value={editSecret}
|
||||
onChange={e => setEditSecret(e.target.value)}
|
||||
onKeyDown={e => e.key === "Enter" && handleSecretSubmit()}
|
||||
className="w-full px-3 py-2 bg-slate-700 border border-slate-600 rounded-lg text-white placeholder-slate-400 focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-transparent mb-3 sm:mb-4 text-sm sm:text-base"
|
||||
autoFocus
|
||||
/>
|
||||
<div className="flex gap-2 sm:gap-3 justify-end">
|
||||
<button
|
||||
onClick={() => {
|
||||
setShowSecretPrompt(false)
|
||||
setEditSecret("")
|
||||
}}
|
||||
className="px-3 sm:px-4 py-1.5 sm:py-2 text-slate-300 hover:text-white transition-colors text-sm sm:text-base"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
onClick={handleSecretSubmit}
|
||||
className="px-3 sm:px-4 py-1.5 sm:py-2 bg-purple-600 hover:bg-purple-700 text-white rounded-lg transition-colors text-sm sm:text-base"
|
||||
>
|
||||
Unlock
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default MonacoDiffViewer
|
||||
|
|
@ -4,16 +4,16 @@ import { cva, type VariantProps } from "class-variance-authority"
|
|||
import { cn } from "@/lib/utils"
|
||||
|
||||
const badgeVariants = cva(
|
||||
"inline-flex items-center rounded-sm border px-2 py-0.5 text-xs font-semibold transition-colors",
|
||||
"inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "border-transparent bg-zinc-700 text-zinc-100 hover:bg-zinc-600",
|
||||
default: "border-transparent bg-primary text-primary-foreground hover:bg-primary/80",
|
||||
secondary:
|
||||
"border-transparent bg-zinc-800 text-zinc-300 hover:bg-zinc-700",
|
||||
"border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||
destructive:
|
||||
"border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
|
||||
outline: "border-zinc-700 text-zinc-300",
|
||||
outline: "text-foreground",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
|
|
@ -31,4 +31,4 @@ function Badge({ className, variant, ...props }: BadgeProps) {
|
|||
return <div className={cn(badgeVariants({ variant }), className)} {...props} />
|
||||
}
|
||||
|
||||
export { Badge, badgeVariants }
|
||||
export { Badge, badgeVariants }
|
||||
|
|
|
|||
|
|
@ -5,22 +5,22 @@ import { cva, type VariantProps } from "class-variance-authority"
|
|||
import { cn } from "@/lib/utils"
|
||||
|
||||
const buttonVariants = cva(
|
||||
"inline-flex items-center justify-center whitespace-nowrap rounded-sm text-sm font-medium ring-offset-background transition-all duration-150 active:scale-[0.98] focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-zinc-600 disabled:pointer-events-none disabled:opacity-50",
|
||||
"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-zinc-100 dark:bg-zinc-700 text-zinc-950 dark:text-zinc-50 hover:bg-zinc-200 dark:hover:bg-zinc-600",
|
||||
default: "bg-primary text-primary-foreground hover:bg-primary/90",
|
||||
destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90",
|
||||
outline: "border border-zinc-700 hover:bg-zinc-800 hover:text-zinc-50",
|
||||
secondary: "bg-zinc-800 text-zinc-50 hover:bg-zinc-700",
|
||||
ghost: "hover:bg-zinc-800 hover:text-zinc-50",
|
||||
link: "text-zinc-400 underline-offset-4 hover:underline hover:text-zinc-300",
|
||||
outline: "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
|
||||
secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||
ghost: "hover:bg-accent hover:text-accent-foreground",
|
||||
link: "text-primary underline-offset-4 hover:underline",
|
||||
},
|
||||
size: {
|
||||
default: "h-9 px-3 py-2",
|
||||
sm: "h-8 rounded-sm px-2",
|
||||
lg: "h-10 rounded-sm px-4",
|
||||
icon: "h-9 w-9",
|
||||
default: "h-10 px-4 py-2",
|
||||
sm: "h-9 rounded-md px-3",
|
||||
lg: "h-11 rounded-md px-8",
|
||||
icon: "h-10 w-10",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ const Card = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElemen
|
|||
({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn("rounded-sm border border-zinc-800 bg-zinc-900 text-zinc-50", className)}
|
||||
className={cn("rounded-lg border bg-card text-card-foreground shadow-sm", className)}
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
|
|
@ -15,7 +15,7 @@ Card.displayName = "Card"
|
|||
|
||||
const CardHeader = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
||||
({ className, ...props }, ref) => (
|
||||
<div ref={ref} className={cn("flex flex-col space-y-1.5 p-4", className)} {...props} />
|
||||
<div ref={ref} className={cn("flex flex-col space-y-1.5 p-6", className)} {...props} />
|
||||
),
|
||||
)
|
||||
CardHeader.displayName = "CardHeader"
|
||||
|
|
@ -24,7 +24,7 @@ const CardTitle = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HT
|
|||
({ className, ...props }, ref) => (
|
||||
<h3
|
||||
ref={ref}
|
||||
className={cn("text-lg font-semibold leading-none tracking-tight", className)}
|
||||
className={cn("text-2xl font-semibold leading-none tracking-tight", className)}
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
|
|
@ -35,20 +35,20 @@ const CardDescription = React.forwardRef<
|
|||
HTMLParagraphElement,
|
||||
React.HTMLAttributes<HTMLParagraphElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<p ref={ref} className={cn("text-sm text-zinc-400", className)} {...props} />
|
||||
<p ref={ref} className={cn("text-sm text-muted-foreground", className)} {...props} />
|
||||
))
|
||||
CardDescription.displayName = "CardDescription"
|
||||
|
||||
const CardContent = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
||||
({ className, ...props }, ref) => (
|
||||
<div ref={ref} className={cn("p-4 pt-0", className)} {...props} />
|
||||
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
|
||||
),
|
||||
)
|
||||
CardContent.displayName = "CardContent"
|
||||
|
||||
const CardFooter = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
||||
({ className, ...props }, ref) => (
|
||||
<div ref={ref} className={cn("flex items-center p-4 pt-0", className)} {...props} />
|
||||
<div ref={ref} className={cn("flex items-center p-6 pt-0", className)} {...props} />
|
||||
),
|
||||
)
|
||||
CardFooter.displayName = "CardFooter"
|
||||
|
|
|
|||
|
|
@ -1,45 +0,0 @@
|
|||
/**
|
||||
* Icon Standardization Examples
|
||||
*
|
||||
* This file demonstrates the standardized icon treatment across components
|
||||
* following the zinc color palette and consistent sizing patterns.
|
||||
*
|
||||
* Icon Guidelines:
|
||||
* - Small contexts (buttons, badges): w-4 h-4
|
||||
* - Medium contexts (headers, titles): w-5 h-5
|
||||
* - Consistent stroke width: strokeWidth={2}
|
||||
* - Color hierarchy: zinc-400 (muted) → zinc-300 (hover) → zinc-50 (active)
|
||||
* - All icons from lucide-react should follow these standards
|
||||
*/
|
||||
|
||||
import { Search, Settings, ChevronRight } from "lucide-react"
|
||||
import { Button } from "./button"
|
||||
|
||||
export function IconExamples() {
|
||||
return (
|
||||
<div className="space-y-4 p-4">
|
||||
{/* Button with icon - w-4 h-4 standard */}
|
||||
<Button>
|
||||
<Search className="mr-2 h-4 w-4 text-zinc-400" strokeWidth={2} />
|
||||
Search
|
||||
</Button>
|
||||
|
||||
{/* Icon button - icon only */}
|
||||
<Button size="icon">
|
||||
<Settings className="h-4 w-4 text-zinc-400 hover:text-zinc-300" strokeWidth={2} />
|
||||
</Button>
|
||||
|
||||
{/* Card header icon - w-5 h-5 for larger contexts */}
|
||||
<div className="flex items-center gap-2">
|
||||
<ChevronRight className="h-5 w-5 text-zinc-400" strokeWidth={2} />
|
||||
<h3 className="text-lg font-semibold">Card Title</h3>
|
||||
</div>
|
||||
|
||||
{/* Active state example */}
|
||||
<Button className="bg-zinc-700">
|
||||
<Search className="mr-2 h-4 w-4 text-zinc-50" strokeWidth={2} />
|
||||
Active Search
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -10,7 +10,7 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
|||
<input
|
||||
type={type}
|
||||
className={cn(
|
||||
"flex h-9 w-full rounded-sm border border-zinc-700 bg-zinc-950 px-3 py-2 text-sm text-zinc-50 ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-zinc-500 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-zinc-600 disabled:cursor-not-allowed disabled:opacity-50",
|
||||
"flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className,
|
||||
)}
|
||||
ref={ref}
|
||||
|
|
|
|||
|
|
@ -19,14 +19,14 @@ const SelectTrigger = React.forwardRef<
|
|||
<SelectPrimitive.Trigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex h-9 w-full items-center justify-between rounded-sm border border-zinc-700 bg-zinc-950 px-3 py-2 text-sm text-zinc-50 ring-offset-background placeholder:text-zinc-500 focus:outline-none focus:ring-1 focus:ring-zinc-600 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
|
||||
"flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<SelectPrimitive.Icon asChild>
|
||||
<ChevronDown className="h-4 w-4 text-zinc-400" strokeWidth={2} />
|
||||
<ChevronDown className="h-4 w-4 opacity-50" />
|
||||
</SelectPrimitive.Icon>
|
||||
</SelectPrimitive.Trigger>
|
||||
))
|
||||
|
|
@ -41,7 +41,7 @@ const SelectScrollUpButton = React.forwardRef<
|
|||
className={cn("flex cursor-default items-center justify-center py-1", className)}
|
||||
{...props}
|
||||
>
|
||||
<ChevronUp className="h-4 w-4 text-zinc-400" strokeWidth={2} />
|
||||
<ChevronUp className="h-4 w-4" />
|
||||
</SelectPrimitive.ScrollUpButton>
|
||||
))
|
||||
SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName
|
||||
|
|
@ -55,7 +55,7 @@ const SelectScrollDownButton = React.forwardRef<
|
|||
className={cn("flex cursor-default items-center justify-center py-1", className)}
|
||||
{...props}
|
||||
>
|
||||
<ChevronDown className="h-4 w-4 text-zinc-400" strokeWidth={2} />
|
||||
<ChevronDown className="h-4 w-4" />
|
||||
</SelectPrimitive.ScrollDownButton>
|
||||
))
|
||||
SelectScrollDownButton.displayName = SelectPrimitive.ScrollDownButton.displayName
|
||||
|
|
@ -69,7 +69,7 @@ const SelectContent = React.forwardRef<
|
|||
<SelectPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-sm border border-zinc-700 bg-zinc-900 text-zinc-50 shadow-sm data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||
"relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||
position === "popper" &&
|
||||
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
|
||||
className,
|
||||
|
|
@ -112,14 +112,14 @@ const SelectItem = React.forwardRef<
|
|||
<SelectPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-zinc-800 focus:text-zinc-50 data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||
<SelectPrimitive.ItemIndicator>
|
||||
<Check className="h-4 w-4 text-zinc-50" strokeWidth={2} />
|
||||
<Check className="h-4 w-4" />
|
||||
</SelectPrimitive.ItemIndicator>
|
||||
</span>
|
||||
|
||||
|
|
@ -134,7 +134,7 @@ const SelectSeparator = React.forwardRef<
|
|||
>(({ className, ...props }, ref) => (
|
||||
<SelectPrimitive.Separator
|
||||
ref={ref}
|
||||
className={cn("-mx-1 my-1 h-px bg-zinc-700", className)}
|
||||
className={cn("-mx-1 my-1 h-px bg-muted", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ const Separator = React.forwardRef<
|
|||
decorative={decorative}
|
||||
orientation={orientation}
|
||||
className={cn(
|
||||
"shrink-0 bg-zinc-800",
|
||||
"shrink-0 bg-border",
|
||||
orientation === "horizontal" ? "h-[1px] w-full" : "h-full w-[1px]",
|
||||
className,
|
||||
)}
|
||||
|
|
@ -24,4 +24,4 @@ const Separator = React.forwardRef<
|
|||
))
|
||||
Separator.displayName = SeparatorPrimitive.Root.displayName
|
||||
|
||||
export { Separator }
|
||||
export { Separator }
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ export function Switch({ checked, onCheckedChange, disabled, className, id }: Sw
|
|||
disabled={disabled}
|
||||
onClick={() => onCheckedChange(!checked)}
|
||||
className={cn(
|
||||
"peer inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-all duration-200 ease-in-out",
|
||||
"peer inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors",
|
||||
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background",
|
||||
"disabled:cursor-not-allowed disabled:opacity-50",
|
||||
checked ? "bg-primary" : "bg-muted-foreground/30",
|
||||
|
|
@ -30,7 +30,7 @@ export function Switch({ checked, onCheckedChange, disabled, className, id }: Sw
|
|||
>
|
||||
<span
|
||||
className={cn(
|
||||
"pointer-events-none block h-4 w-4 rounded-full bg-background shadow-lg ring-0 transition-all duration-200 ease-in-out",
|
||||
"pointer-events-none block h-4 w-4 rounded-full bg-background shadow-lg ring-0 transition-transform",
|
||||
checked ? "translate-x-4" : "translate-x-0",
|
||||
)}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ const TableFooter = React.forwardRef<
|
|||
>(({ className, ...props }, ref) => (
|
||||
<tfoot
|
||||
ref={ref}
|
||||
className={cn("border-t border-zinc-800 bg-zinc-900 font-medium [&>tr]:last:border-b-0", className)}
|
||||
className={cn("border-t bg-muted/50 font-medium [&>tr]:last:border-b-0", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
|
|
@ -44,7 +44,7 @@ const TableRow = React.forwardRef<HTMLTableRowElement, React.HTMLAttributes<HTML
|
|||
<tr
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"border-b border-zinc-800 transition-colors hover:bg-zinc-800/50 data-[state=selected]:bg-zinc-800",
|
||||
"border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
|
|
@ -60,7 +60,7 @@ const TableHead = React.forwardRef<
|
|||
<th
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"h-10 px-3 py-2 text-left align-middle font-medium font-mono text-zinc-400 [&:has([role=checkbox])]:pr-0",
|
||||
"h-12 px-4 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
|
|
@ -74,7 +74,7 @@ const TableCell = React.forwardRef<
|
|||
>(({ className, ...props }, ref) => (
|
||||
<td
|
||||
ref={ref}
|
||||
className={cn("p-3 align-middle [&:has([role=checkbox])]:pr-0", className)}
|
||||
className={cn("p-4 align-middle [&:has([role=checkbox])]:pr-0", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
|
|
@ -88,4 +88,4 @@ const TableCaption = React.forwardRef<
|
|||
))
|
||||
TableCaption.displayName = "TableCaption"
|
||||
|
||||
export { Table, TableHeader, TableBody, TableFooter, TableHead, TableRow, TableCell, TableCaption }
|
||||
export { Table, TableHeader, TableBody, TableFooter, TableHead, TableRow, TableCell, TableCaption }
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ const TabsTrigger = React.forwardRef<
|
|||
<TabsPrimitive.Trigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"inline-flex items-center justify-center whitespace-nowrap rounded-sm px-3 py-1.5 text-sm font-medium ring-offset-background transition-all duration-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow-sm",
|
||||
"inline-flex items-center justify-center whitespace-nowrap rounded-sm px-3 py-1.5 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow-sm",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
|
|
@ -45,7 +45,7 @@ const TabsContent = React.forwardRef<
|
|||
<TabsPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"mt-2 ring-offset-background transition-opacity duration-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
|
||||
"mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
|
|
|
|||
68
js/cf-webapp/src/lib/observability-response-parse.ts
Normal file
68
js/cf-webapp/src/lib/observability-response-parse.ts
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
/**
|
||||
* Helpers to parse LLM raw_response for observability display.
|
||||
* Ranking responses use <rank> and <explain> tags; optimization uses markdown code blocks.
|
||||
* raw_response is often the full API JSON (e.g. OpenAI); we extract message content when present.
|
||||
*/
|
||||
|
||||
/** Extract message content from OpenAI-style API response JSON, or return null */
|
||||
export function extractMessageContentFromApiResponse(raw: string): string | null {
|
||||
try {
|
||||
const parsed = JSON.parse(raw) as {
|
||||
choices?: Array<{ message?: { content?: string } }>
|
||||
}
|
||||
const content = parsed?.choices?.[0]?.message?.content
|
||||
return typeof content === "string" ? content : null
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
/** Get the string to use for rank/explain and markdown parsing (inner content if API JSON, else raw) */
|
||||
export function getResponseContentForParsing(rawResponse: string): string {
|
||||
return extractMessageContentFromApiResponse(rawResponse) ?? rawResponse
|
||||
}
|
||||
|
||||
/** Extract content inside <rank>...</rank> */
|
||||
export function extractRankTag(content: string): string | null {
|
||||
const m = content.match(/<rank>([\s\S]*?)<\/rank>/i)
|
||||
return m ? m[1].trim() : null
|
||||
}
|
||||
|
||||
/** Extract content inside <explain>...</explain> */
|
||||
export function extractExplainTag(content: string): string | null {
|
||||
const m = content.match(/<explain>([\s\S]*?)<\/explain>/i)
|
||||
return m ? m[1].trim() : null
|
||||
}
|
||||
|
||||
export type ResponseSegment =
|
||||
| { kind: "text"; content: string }
|
||||
| { kind: "code"; language: string; content: string }
|
||||
|
||||
/**
|
||||
* Split markdown-like content into text and code blocks (```lang ... ```).
|
||||
* Language is taken from the first word after ```; default "text".
|
||||
*/
|
||||
export function splitMarkdownCodeBlocks(content: string): ResponseSegment[] {
|
||||
const segments: ResponseSegment[] = []
|
||||
const re = /```(\w*)\n?([\s\S]*?)```/g
|
||||
let lastEnd = 0
|
||||
let m: RegExpExecArray | null
|
||||
while ((m = re.exec(content)) !== null) {
|
||||
if (m.index > lastEnd) {
|
||||
const text = content.slice(lastEnd, m.index)
|
||||
if (text.trim()) {
|
||||
segments.push({ kind: "text", content: text })
|
||||
}
|
||||
}
|
||||
const lang = m[1] || "text"
|
||||
segments.push({ kind: "code", language: lang, content: m[2].trim() })
|
||||
lastEnd = re.lastIndex
|
||||
}
|
||||
if (lastEnd < content.length) {
|
||||
const text = content.slice(lastEnd)
|
||||
if (text.trim()) {
|
||||
segments.push({ kind: "text", content: text })
|
||||
}
|
||||
}
|
||||
return segments
|
||||
}
|
||||
38
js/cf-webapp/src/lib/observability-utils.ts
Normal file
38
js/cf-webapp/src/lib/observability-utils.ts
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
// Shared utilities for observability pages
|
||||
|
||||
/**
|
||||
* Determines the source of an LLM call based on event_type and context
|
||||
*/
|
||||
export function getCallSource(
|
||||
eventType: string | null,
|
||||
context: Record<string, unknown> | null,
|
||||
): string {
|
||||
if (
|
||||
context &&
|
||||
typeof context === "object" &&
|
||||
!Array.isArray(context) &&
|
||||
"source" in context
|
||||
) {
|
||||
return String(context.source)
|
||||
}
|
||||
if (eventType) {
|
||||
if (eventType === "pr_created" || eventType === "pr_merged" || eventType === "pr_closed") {
|
||||
return "GitHub Action"
|
||||
}
|
||||
if (eventType === "no-pr") {
|
||||
return "CLI/VSCode"
|
||||
}
|
||||
return eventType
|
||||
}
|
||||
return "Unknown"
|
||||
}
|
||||
|
||||
/**
|
||||
* Safely extracts cost and tokens from nullable values
|
||||
*/
|
||||
export function safeCostTokens(cost: number | null, tokens: number | null) {
|
||||
return {
|
||||
cost: cost ?? 0,
|
||||
tokens: tokens ?? 0,
|
||||
}
|
||||
}
|
||||
|
|
@ -53,6 +53,7 @@ export const config = {
|
|||
matcher: [
|
||||
"/",
|
||||
"/app/:path*",
|
||||
"/trace/:path*",
|
||||
"/billing",
|
||||
"/billing/:path*",
|
||||
"/apikeys",
|
||||
|
|
|
|||
|
|
@ -1,47 +0,0 @@
|
|||
/* Spacing and Layout Token System */
|
||||
/* Based on 8px grid for consistent rhythm */
|
||||
|
||||
:root {
|
||||
/* Base spacing unit - foundation of the 8px grid */
|
||||
--space-unit: 8px;
|
||||
|
||||
/* Border Radius Tokens */
|
||||
/* Professional, subtle radius values (2-4px max except for pills) */
|
||||
--radius-sm: 2px; /* Tight, professional for small elements */
|
||||
--radius-md: 3px; /* Default for cards, panels, buttons */
|
||||
--radius-lg: 4px; /* Maximum for larger elements */
|
||||
--radius-full: 9999px; /* Only for pills, badges, circular elements */
|
||||
|
||||
/* Spacing Tokens - 8px increments */
|
||||
--space-0: 0px; /* 0 * 8px */
|
||||
--space-px: 1px; /* For borders, hairlines */
|
||||
--space-0\.5: 4px; /* 0.5 * 8px - fine adjustments */
|
||||
--space-1: 8px; /* 1 * 8px */
|
||||
--space-2: 16px; /* 2 * 8px */
|
||||
--space-3: 24px; /* 3 * 8px */
|
||||
--space-4: 32px; /* 4 * 8px */
|
||||
--space-5: 40px; /* 5 * 8px */
|
||||
--space-6: 48px; /* 6 * 8px */
|
||||
--space-7: 56px; /* 7 * 8px */
|
||||
--space-8: 64px; /* 8 * 8px */
|
||||
--space-9: 72px; /* 9 * 8px */
|
||||
--space-10: 80px; /* 10 * 8px */
|
||||
|
||||
/* Common Layout Spacing Patterns */
|
||||
/* These map to the base spacing tokens for consistency */
|
||||
--space-gap-xs: var(--space-1); /* 8px - Tight spacing */
|
||||
--space-gap-sm: var(--space-2); /* 16px - Small spacing */
|
||||
--space-gap-md: var(--space-3); /* 24px - Medium spacing */
|
||||
--space-gap-lg: var(--space-4); /* 32px - Large spacing */
|
||||
--space-gap-xl: var(--space-5); /* 40px - Extra large spacing */
|
||||
|
||||
/* Container and Content Spacing */
|
||||
--space-container-sm: var(--space-2); /* 16px - Mobile/small screens */
|
||||
--space-container-md: var(--space-3); /* 24px - Tablet/medium screens */
|
||||
--space-container-lg: var(--space-4); /* 32px - Desktop/large screens */
|
||||
|
||||
/* Card and Panel Internal Spacing */
|
||||
--space-card-sm: var(--space-2); /* 16px - Compact cards */
|
||||
--space-card-md: var(--space-3); /* 24px - Standard cards */
|
||||
--space-card-lg: var(--space-4); /* 32px - Spacious cards */
|
||||
}
|
||||
|
|
@ -1,184 +0,0 @@
|
|||
/**
|
||||
* Design Token System
|
||||
*
|
||||
* Professional developer-focused design tokens using CSS custom properties.
|
||||
* Dark mode only, zinc color scale, semantic status colors.
|
||||
*
|
||||
* RGB format (e.g., "24 24 27") for Tailwind's alpha channel support.
|
||||
*/
|
||||
|
||||
:root {
|
||||
/* ============================================
|
||||
* ZINC COLOR SCALE
|
||||
* Neutral colors for all UI elements
|
||||
* RGB values without rgb() wrapper for Tailwind
|
||||
* ============================================ */
|
||||
|
||||
/* Lightest to darkest */
|
||||
--color-zinc-50: 250 250 250; /* Almost white */
|
||||
--color-zinc-100: 244 244 245; /* Very light gray */
|
||||
--color-zinc-200: 228 228 231; /* Light gray */
|
||||
--color-zinc-300: 212 212 216; /* Medium light gray */
|
||||
--color-zinc-400: 161 161 170; /* Medium gray */
|
||||
--color-zinc-500: 113 113 122; /* True medium gray */
|
||||
--color-zinc-600: 82 82 91; /* Medium dark gray */
|
||||
--color-zinc-700: 63 63 70; /* Dark gray */
|
||||
--color-zinc-800: 39 39 42; /* Very dark gray */
|
||||
--color-zinc-900: 24 24 27; /* Near black */
|
||||
--color-zinc-950: 9 9 11; /* Deep black */
|
||||
|
||||
/* ============================================
|
||||
* SEMANTIC STATUS COLORS
|
||||
* For errors, warnings, and success states
|
||||
* ============================================ */
|
||||
|
||||
/* Error states (red-based) */
|
||||
--color-error: 239 68 68; /* red-500 - Primary error */
|
||||
--color-error-foreground: 254 242 242; /* red-50 - Error text on error bg */
|
||||
--color-error-muted: 254 202 202; /* red-200 - Subtle error background */
|
||||
--color-error-border: 248 113 113; /* red-400 - Error borders */
|
||||
|
||||
/* Success states (green-based) */
|
||||
--color-success: 34 197 94; /* green-500 - Primary success */
|
||||
--color-success-foreground: 240 253 244; /* green-50 - Success text on success bg */
|
||||
--color-success-muted: 187 247 208; /* green-200 - Subtle success background */
|
||||
--color-success-border: 74 222 128; /* green-400 - Success borders */
|
||||
|
||||
/* Warning states (amber-based, not bright yellow) */
|
||||
--color-warning: 245 158 11; /* amber-500 - Primary warning */
|
||||
--color-warning-foreground: 255 251 235; /* amber-50 - Warning text on warning bg */
|
||||
--color-warning-muted: 254 215 170; /* amber-200 - Subtle warning background */
|
||||
--color-warning-border: 251 191 36; /* amber-400 - Warning borders */
|
||||
|
||||
/* Info states (blue-based) */
|
||||
--color-info: 59 130 246; /* blue-500 - Primary info */
|
||||
--color-info-foreground: 239 246 255; /* blue-50 - Info text on info bg */
|
||||
--color-info-muted: 191 219 254; /* blue-200 - Subtle info background */
|
||||
--color-info-border: 96 165 250; /* blue-400 - Info borders */
|
||||
|
||||
/* ============================================
|
||||
* FUNCTIONAL TOKENS
|
||||
* High-level tokens that reference color scale
|
||||
* These are what components should use
|
||||
* ============================================ */
|
||||
|
||||
/* Core UI surfaces */
|
||||
--background: var(--color-zinc-950); /* Main app background */
|
||||
--foreground: var(--color-zinc-50); /* Main text color */
|
||||
|
||||
/* Card and panel surfaces */
|
||||
--card: var(--color-zinc-900); /* Card background */
|
||||
--card-foreground: var(--color-zinc-50); /* Card text */
|
||||
|
||||
/* Popover/dropdown surfaces */
|
||||
--popover: var(--color-zinc-900); /* Popover background */
|
||||
--popover-foreground: var(--color-zinc-50); /* Popover text */
|
||||
|
||||
/* Interactive elements */
|
||||
--primary: var(--color-zinc-50); /* Primary button bg */
|
||||
--primary-foreground: var(--color-zinc-950); /* Primary button text */
|
||||
|
||||
--secondary: var(--color-zinc-800); /* Secondary button bg */
|
||||
--secondary-foreground: var(--color-zinc-50); /* Secondary button text */
|
||||
|
||||
--accent: var(--color-zinc-700); /* Accent elements */
|
||||
--accent-foreground: var(--color-zinc-50); /* Accent text */
|
||||
|
||||
/* Muted states */
|
||||
--muted: var(--color-zinc-800); /* Muted backgrounds */
|
||||
--muted-foreground: var(--color-zinc-400); /* Muted text */
|
||||
|
||||
/* Destructive actions */
|
||||
--destructive: var(--color-error); /* Destructive button bg */
|
||||
--destructive-foreground: var(--color-error-foreground); /* Destructive button text */
|
||||
|
||||
/* Borders and inputs */
|
||||
--border: var(--color-zinc-800); /* Default borders */
|
||||
--input: var(--color-zinc-800); /* Input borders */
|
||||
--ring: var(--color-zinc-600); /* Focus rings */
|
||||
|
||||
/* ============================================
|
||||
* TYPOGRAPHY TOKENS
|
||||
* Font stacks and text properties
|
||||
* ============================================ */
|
||||
|
||||
/* Font families */
|
||||
--font-mono: 'JetBrains Mono', 'Fira Code', 'Cascadia Code', 'Roboto Mono', ui-monospace, 'Courier New', monospace;
|
||||
--font-sans: system-ui, -apple-system, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||
|
||||
/* Font sizes - scale for data-dense layouts */
|
||||
--text-xs: 0.75rem; /* 12px */
|
||||
--text-sm: 0.875rem; /* 14px */
|
||||
--text-base: 1rem; /* 16px */
|
||||
--text-lg: 1.125rem; /* 18px */
|
||||
--text-xl: 1.25rem; /* 20px */
|
||||
--text-2xl: 1.5rem; /* 24px */
|
||||
|
||||
/* Line heights */
|
||||
--leading-tight: 1.25;
|
||||
--leading-normal: 1.5;
|
||||
--leading-relaxed: 1.625;
|
||||
|
||||
/* ============================================
|
||||
* SPACING TOKENS
|
||||
* 8px grid system
|
||||
* ============================================ */
|
||||
|
||||
--space-unit: 8px;
|
||||
--space-0: 0;
|
||||
--space-0-5: 4px; /* Half unit for fine adjustments */
|
||||
--space-1: 8px; /* Base unit */
|
||||
--space-1-5: 12px;
|
||||
--space-2: 16px;
|
||||
--space-2-5: 20px;
|
||||
--space-3: 24px;
|
||||
--space-4: 32px;
|
||||
--space-5: 40px;
|
||||
--space-6: 48px;
|
||||
--space-7: 56px;
|
||||
--space-8: 64px;
|
||||
--space-9: 72px;
|
||||
--space-10: 80px;
|
||||
|
||||
/* ============================================
|
||||
* BORDER RADIUS TOKENS
|
||||
* Flat, minimal rounded corners
|
||||
* ============================================ */
|
||||
|
||||
--radius-none: 0;
|
||||
--radius-sm: 2px; /* Subtle rounding */
|
||||
--radius-md: 3px; /* Default */
|
||||
--radius-lg: 4px; /* Maximum rounding */
|
||||
--radius: var(--radius-md); /* Default radius */
|
||||
|
||||
/* ============================================
|
||||
* SHADOW TOKENS
|
||||
* Minimal shadows for depth
|
||||
* ============================================ */
|
||||
|
||||
--shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.3);
|
||||
--shadow-md: 0 2px 4px 0 rgba(0, 0, 0, 0.4);
|
||||
--shadow-lg: 0 4px 6px 0 rgba(0, 0, 0, 0.5);
|
||||
--shadow-xl: 0 8px 16px 0 rgba(0, 0, 0, 0.6);
|
||||
|
||||
/* ============================================
|
||||
* TRANSITION TOKENS
|
||||
* Consistent animation timing
|
||||
* ============================================ */
|
||||
|
||||
--transition-fast: 150ms;
|
||||
--transition-base: 250ms;
|
||||
--transition-slow: 350ms;
|
||||
--ease-in-out: cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
* DARK MODE ONLY
|
||||
* We're not supporting light mode
|
||||
* This ensures dark mode is always active
|
||||
* ============================================ */
|
||||
|
||||
.dark {
|
||||
/* All tokens are already defined for dark mode in :root */
|
||||
/* This class exists for Tailwind's dark: variant to work */
|
||||
}
|
||||
|
|
@ -1,29 +0,0 @@
|
|||
/* Typography Token System */
|
||||
|
||||
:root {
|
||||
/* Font Family Tokens */
|
||||
--font-mono: 'JetBrains Mono', 'Fira Code', ui-monospace, 'SF Mono', 'Monaco', 'Cascadia Code', monospace;
|
||||
--font-sans: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||
|
||||
/* Font Size Scale - optimized for dark mode readability */
|
||||
--text-xs: 0.75rem; /* 12px */
|
||||
--text-sm: 0.875rem; /* 14px */
|
||||
--text-base: 1rem; /* 16px */
|
||||
--text-lg: 1.125rem; /* 18px */
|
||||
--text-xl: 1.25rem; /* 20px */
|
||||
--text-2xl: 1.5rem; /* 24px */
|
||||
--text-code: 0.875rem; /* 14px - inline code specific */
|
||||
|
||||
/* Font Weight Tokens - optimized for dark backgrounds */
|
||||
--font-light: 300;
|
||||
--font-normal: 400;
|
||||
--font-medium: 500;
|
||||
--font-semibold: 600;
|
||||
--font-bold: 700;
|
||||
|
||||
/* Line Height Tokens - for data density control */
|
||||
--leading-tight: 1.25;
|
||||
--leading-normal: 1.5;
|
||||
--leading-relaxed: 1.75;
|
||||
--leading-code: 1.4; /* Specific for code blocks */
|
||||
}
|
||||
|
|
@ -1,108 +1,20 @@
|
|||
import type { Config } from "tailwindcss"
|
||||
import { fontFamily } from "tailwindcss/defaultTheme"
|
||||
|
||||
const config: Config = {
|
||||
darkMode: 'class',
|
||||
content: [
|
||||
"./src/pages/**/*.{js,ts,jsx,tsx,mdx}",
|
||||
"./src/components/**/*.{js,ts,jsx,tsx,mdx}",
|
||||
"./src/app/**/*.{js,ts,jsx,tsx,mdx}",
|
||||
],
|
||||
theme: {
|
||||
spacing: {
|
||||
'px': '1px',
|
||||
'0': '0',
|
||||
'0.5': '4px',
|
||||
'1': '8px',
|
||||
'2': '16px',
|
||||
'3': '24px',
|
||||
'4': '32px',
|
||||
'5': '40px',
|
||||
'6': '48px',
|
||||
'7': '56px',
|
||||
'8': '64px',
|
||||
'9': '72px',
|
||||
'10': '80px',
|
||||
},
|
||||
borderRadius: {
|
||||
'none': '0px',
|
||||
'sm': 'var(--radius-sm)',
|
||||
DEFAULT: 'var(--radius-md)',
|
||||
'md': 'var(--radius-md)',
|
||||
'lg': 'var(--radius-lg)',
|
||||
'full': 'var(--radius-full)',
|
||||
},
|
||||
extend: {
|
||||
colors: {
|
||||
zinc: {
|
||||
'50': 'rgb(var(--color-zinc-50) / <alpha-value>)',
|
||||
'100': 'rgb(var(--color-zinc-100) / <alpha-value>)',
|
||||
'200': 'rgb(var(--color-zinc-200) / <alpha-value>)',
|
||||
'300': 'rgb(var(--color-zinc-300) / <alpha-value>)',
|
||||
'400': 'rgb(var(--color-zinc-400) / <alpha-value>)',
|
||||
'500': 'rgb(var(--color-zinc-500) / <alpha-value>)',
|
||||
'600': 'rgb(var(--color-zinc-600) / <alpha-value>)',
|
||||
'700': 'rgb(var(--color-zinc-700) / <alpha-value>)',
|
||||
'800': 'rgb(var(--color-zinc-800) / <alpha-value>)',
|
||||
'900': 'rgb(var(--color-zinc-900) / <alpha-value>)',
|
||||
'950': 'rgb(var(--color-zinc-950) / <alpha-value>)',
|
||||
},
|
||||
background: 'rgb(var(--background) / <alpha-value>)',
|
||||
foreground: 'rgb(var(--foreground) / <alpha-value>)',
|
||||
card: {
|
||||
DEFAULT: 'rgb(var(--card) / <alpha-value>)',
|
||||
foreground: 'rgb(var(--card-foreground) / <alpha-value>)',
|
||||
},
|
||||
popover: {
|
||||
DEFAULT: 'rgb(var(--popover) / <alpha-value>)',
|
||||
foreground: 'rgb(var(--popover-foreground) / <alpha-value>)',
|
||||
},
|
||||
primary: {
|
||||
DEFAULT: 'rgb(var(--primary) / <alpha-value>)',
|
||||
foreground: 'rgb(var(--primary-foreground) / <alpha-value>)',
|
||||
},
|
||||
secondary: {
|
||||
DEFAULT: 'rgb(var(--secondary) / <alpha-value>)',
|
||||
foreground: 'rgb(var(--secondary-foreground) / <alpha-value>)',
|
||||
},
|
||||
muted: {
|
||||
DEFAULT: 'rgb(var(--muted) / <alpha-value>)',
|
||||
foreground: 'rgb(var(--muted-foreground) / <alpha-value>)',
|
||||
},
|
||||
accent: {
|
||||
DEFAULT: 'rgb(var(--accent) / <alpha-value>)',
|
||||
foreground: 'rgb(var(--accent-foreground) / <alpha-value>)',
|
||||
},
|
||||
error: {
|
||||
DEFAULT: 'rgb(var(--error) / <alpha-value>)',
|
||||
foreground: 'rgb(var(--error-foreground) / <alpha-value>)',
|
||||
muted: 'rgb(var(--error-muted) / <alpha-value>)',
|
||||
border: 'rgb(var(--error-border) / <alpha-value>)',
|
||||
},
|
||||
warning: {
|
||||
DEFAULT: 'rgb(var(--warning) / <alpha-value>)',
|
||||
foreground: 'rgb(var(--warning-foreground) / <alpha-value>)',
|
||||
muted: 'rgb(var(--warning-muted) / <alpha-value>)',
|
||||
border: 'rgb(var(--warning-border) / <alpha-value>)',
|
||||
},
|
||||
success: {
|
||||
DEFAULT: 'rgb(var(--success) / <alpha-value>)',
|
||||
foreground: 'rgb(var(--success-foreground) / <alpha-value>)',
|
||||
muted: 'rgb(var(--success-muted) / <alpha-value>)',
|
||||
border: 'rgb(var(--success-border) / <alpha-value>)',
|
||||
},
|
||||
info: {
|
||||
DEFAULT: 'rgb(var(--info) / <alpha-value>)',
|
||||
foreground: 'rgb(var(--info-foreground) / <alpha-value>)',
|
||||
muted: 'rgb(var(--info-muted) / <alpha-value>)',
|
||||
border: 'rgb(var(--info-border) / <alpha-value>)',
|
||||
},
|
||||
border: 'rgb(var(--border) / <alpha-value>)',
|
||||
input: 'rgb(var(--input) / <alpha-value>)',
|
||||
ring: 'rgb(var(--ring) / <alpha-value>)',
|
||||
backgroundImage: {
|
||||
"gradient-radial": "radial-gradient(var(--tw-gradient-stops))",
|
||||
"gradient-conic": "conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))",
|
||||
},
|
||||
fontFamily: {
|
||||
sans: ['var(--font-sans)'],
|
||||
mono: ['var(--font-mono)'],
|
||||
sans: ["var(--font-sans)", ...fontFamily.sans],
|
||||
},
|
||||
keyframes: {
|
||||
shimmer: {
|
||||
|
|
|
|||
6
js/common/package-lock.json
generated
6
js/common/package-lock.json
generated
|
|
@ -566,6 +566,7 @@
|
|||
"integrity": "sha512-uug3FEEGv0r+jrecvUUpbY8lLisvIjg6AAic6a2bSP5OEOLeJsDSnvhCDov7ipFFMXS3orMpzlmi0ZcuGkBbow==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"undici-types": "~6.21.0"
|
||||
}
|
||||
|
|
@ -616,6 +617,7 @@
|
|||
"integrity": "sha512-gTtSdWX9xiMPA/7MV9STjJOOYtWwIJIYxkQxnSV1U3xcE+mnJSH3f6zI0RYP+ew66WSlZ5ed+h0VCxsvdC1jJg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@typescript-eslint/scope-manager": "8.41.0",
|
||||
"@typescript-eslint/types": "8.41.0",
|
||||
|
|
@ -881,6 +883,7 @@
|
|||
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"acorn": "bin/acorn"
|
||||
},
|
||||
|
|
@ -1890,6 +1893,7 @@
|
|||
"deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.2.0",
|
||||
"@eslint-community/regexpp": "^4.6.1",
|
||||
|
|
@ -4075,6 +4079,7 @@
|
|||
"devOptional": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@prisma/config": "6.15.0",
|
||||
"@prisma/engines": "6.15.0"
|
||||
|
|
@ -5041,6 +5046,7 @@
|
|||
"integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==",
|
||||
"devOptional": true,
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
|
|
|
|||
Loading…
Reference in a new issue