feat: decision tree visualization with React Flow + Dagre auto-layout
This commit is contained in:
@@ -13,13 +13,16 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@agentlens/database": "*",
|
"@agentlens/database": "*",
|
||||||
|
"@dagrejs/dagre": "^2.0.4",
|
||||||
|
"@xyflow/react": "^12.10.0",
|
||||||
|
"lucide-react": "^0.469.0",
|
||||||
"next": "^15.1.0",
|
"next": "^15.1.0",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
"react-dom": "^19.0.0",
|
"react-dom": "^19.0.0"
|
||||||
"lucide-react": "^0.469.0"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tailwindcss/postcss": "^4.0.0",
|
"@tailwindcss/postcss": "^4.0.0",
|
||||||
|
"@types/dagre": "^0.7.53",
|
||||||
"@types/node": "^22.0.0",
|
"@types/node": "^22.0.0",
|
||||||
"@types/react": "^19.0.0",
|
"@types/react": "^19.0.0",
|
||||||
"@types/react-dom": "^19.0.0",
|
"@types/react-dom": "^19.0.0",
|
||||||
|
|||||||
@@ -38,7 +38,44 @@ export async function GET(
|
|||||||
return NextResponse.json({ error: "Trace not found" }, { status: 404 });
|
return NextResponse.json({ error: "Trace not found" }, { status: 404 });
|
||||||
}
|
}
|
||||||
|
|
||||||
return NextResponse.json({ trace }, { status: 200 });
|
// Transform data to match frontend expectations
|
||||||
|
const transformedTrace = {
|
||||||
|
...trace,
|
||||||
|
decisionPoints: trace.decisionPoints.map((dp) => ({
|
||||||
|
id: dp.id,
|
||||||
|
type: dp.type,
|
||||||
|
chosenAction: typeof dp.chosen === "string" ? dp.chosen : JSON.stringify(dp.chosen),
|
||||||
|
alternatives: dp.alternatives.map((alt) => (typeof alt === "string" ? alt : JSON.stringify(alt))),
|
||||||
|
reasoning: dp.reasoning,
|
||||||
|
contextSnapshot: dp.contextSnapshot as Record<string, unknown> | null,
|
||||||
|
confidence: null, // Not in schema, default to null
|
||||||
|
timestamp: dp.timestamp.toISOString(),
|
||||||
|
parentSpanId: dp.parentSpanId,
|
||||||
|
})),
|
||||||
|
spans: trace.spans.map((span) => ({
|
||||||
|
id: span.id,
|
||||||
|
name: span.name,
|
||||||
|
type: span.type,
|
||||||
|
status: span.status === "COMPLETED" ? "OK" : span.status === "ERROR" ? "ERROR" : "CANCELLED",
|
||||||
|
startedAt: span.startedAt.toISOString(),
|
||||||
|
endedAt: span.endedAt?.toISOString() ?? null,
|
||||||
|
durationMs: span.durationMs,
|
||||||
|
input: span.input,
|
||||||
|
output: span.output,
|
||||||
|
metadata: (span.metadata as Record<string, unknown>) ?? {},
|
||||||
|
parentSpanId: span.parentSpanId,
|
||||||
|
})),
|
||||||
|
events: trace.events.map((event) => ({
|
||||||
|
id: event.id,
|
||||||
|
type: event.type,
|
||||||
|
name: event.name,
|
||||||
|
timestamp: event.timestamp.toISOString(),
|
||||||
|
metadata: (event.metadata as Record<string, unknown>) ?? {},
|
||||||
|
spanId: event.spanId,
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
|
||||||
|
return NextResponse.json({ trace: transformedTrace }, { status: 200 });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error retrieving trace:", error);
|
console.error("Error retrieving trace:", error);
|
||||||
return NextResponse.json({ error: "Internal server error" }, { status: 500 });
|
return NextResponse.json({ error: "Internal server error" }, { status: 500 });
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
import { notFound } from "next/navigation";
|
import { notFound } from "next/navigation";
|
||||||
import { TraceDetail } from "@/components/trace-detail";
|
import { TraceDetail } from "@/components/trace-detail";
|
||||||
|
|
||||||
interface TraceResponse {
|
interface TraceData {
|
||||||
trace: {
|
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
status: "RUNNING" | "COMPLETED" | "ERROR";
|
status: "RUNNING" | "COMPLETED" | "ERROR";
|
||||||
@@ -12,7 +11,7 @@ interface TraceResponse {
|
|||||||
tags: string[];
|
tags: string[];
|
||||||
metadata: Record<string, unknown>;
|
metadata: Record<string, unknown>;
|
||||||
costUsd: number | null;
|
costUsd: number | null;
|
||||||
};
|
totalCost: number | null;
|
||||||
decisionPoints: Array<{
|
decisionPoints: Array<{
|
||||||
id: string;
|
id: string;
|
||||||
type: string;
|
type: string;
|
||||||
@@ -22,6 +21,7 @@ interface TraceResponse {
|
|||||||
contextSnapshot: Record<string, unknown> | null;
|
contextSnapshot: Record<string, unknown> | null;
|
||||||
confidence: number | null;
|
confidence: number | null;
|
||||||
timestamp: string;
|
timestamp: string;
|
||||||
|
parentSpanId: string | null;
|
||||||
}>;
|
}>;
|
||||||
spans: Array<{
|
spans: Array<{
|
||||||
id: string;
|
id: string;
|
||||||
@@ -34,6 +34,7 @@ interface TraceResponse {
|
|||||||
input: unknown;
|
input: unknown;
|
||||||
output: unknown;
|
output: unknown;
|
||||||
metadata: Record<string, unknown>;
|
metadata: Record<string, unknown>;
|
||||||
|
parentSpanId: string | null;
|
||||||
}>;
|
}>;
|
||||||
events: Array<{
|
events: Array<{
|
||||||
id: string;
|
id: string;
|
||||||
@@ -41,9 +42,14 @@ interface TraceResponse {
|
|||||||
name: string;
|
name: string;
|
||||||
timestamp: string;
|
timestamp: string;
|
||||||
metadata: Record<string, unknown>;
|
metadata: Record<string, unknown>;
|
||||||
|
spanId: string | null;
|
||||||
}>;
|
}>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface TraceResponse {
|
||||||
|
trace: TraceData;
|
||||||
|
}
|
||||||
|
|
||||||
async function getTrace(id: string): Promise<TraceResponse | null> {
|
async function getTrace(id: string): Promise<TraceResponse | null> {
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`http://localhost:3000/api/traces/${id}`, {
|
const res = await fetch(`http://localhost:3000/api/traces/${id}`, {
|
||||||
@@ -76,12 +82,24 @@ export default async function TraceDetailPage({ params }: TraceDetailPageProps)
|
|||||||
notFound();
|
notFound();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const { trace } = data;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TraceDetail
|
<TraceDetail
|
||||||
trace={data.trace}
|
trace={{
|
||||||
decisionPoints={data.decisionPoints}
|
id: trace.id,
|
||||||
spans={data.spans}
|
name: trace.name,
|
||||||
events={data.events}
|
status: trace.status,
|
||||||
|
startedAt: trace.startedAt,
|
||||||
|
endedAt: trace.endedAt,
|
||||||
|
durationMs: trace.durationMs,
|
||||||
|
tags: trace.tags,
|
||||||
|
metadata: trace.metadata,
|
||||||
|
costUsd: trace.costUsd ?? trace.totalCost,
|
||||||
|
}}
|
||||||
|
decisionPoints={trace.decisionPoints}
|
||||||
|
spans={trace.spans}
|
||||||
|
events={trace.events}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
797
apps/web/src/components/decision-tree.tsx
Normal file
797
apps/web/src/components/decision-tree.tsx
Normal file
@@ -0,0 +1,797 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useCallback, useMemo, useState } from "react";
|
||||||
|
import {
|
||||||
|
ReactFlow,
|
||||||
|
useNodesState,
|
||||||
|
useEdgesState,
|
||||||
|
addEdge,
|
||||||
|
type Connection,
|
||||||
|
type Edge,
|
||||||
|
type Node,
|
||||||
|
Handle,
|
||||||
|
Position,
|
||||||
|
MiniMap,
|
||||||
|
Controls,
|
||||||
|
Background,
|
||||||
|
BackgroundVariant,
|
||||||
|
ConnectionLineType,
|
||||||
|
} from "@xyflow/react";
|
||||||
|
import "@xyflow/react/dist/style.css";
|
||||||
|
import dagre from "@dagrejs/dagre";
|
||||||
|
import {
|
||||||
|
GitBranch,
|
||||||
|
Activity,
|
||||||
|
Zap,
|
||||||
|
Terminal,
|
||||||
|
AlertCircle,
|
||||||
|
CheckCircle,
|
||||||
|
XCircle,
|
||||||
|
Clock,
|
||||||
|
ChevronDown,
|
||||||
|
ChevronRight,
|
||||||
|
Lightbulb,
|
||||||
|
Layers,
|
||||||
|
} from "lucide-react";
|
||||||
|
import { cn, formatDuration } from "@/lib/utils";
|
||||||
|
|
||||||
|
// Types matching trace-detail.tsx
|
||||||
|
type TraceStatus = "RUNNING" | "COMPLETED" | "ERROR";
|
||||||
|
|
||||||
|
interface DecisionPoint {
|
||||||
|
id: string;
|
||||||
|
type: string;
|
||||||
|
chosenAction: string;
|
||||||
|
alternatives: string[];
|
||||||
|
reasoning: string | null;
|
||||||
|
contextSnapshot: Record<string, unknown> | null;
|
||||||
|
confidence: number | null;
|
||||||
|
timestamp: string;
|
||||||
|
parentSpanId?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Span {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
type: string;
|
||||||
|
status: "OK" | "ERROR" | "CANCELLED";
|
||||||
|
startedAt: string;
|
||||||
|
endedAt: string | null;
|
||||||
|
durationMs: number | null;
|
||||||
|
input: unknown;
|
||||||
|
output: unknown;
|
||||||
|
metadata: Record<string, unknown>;
|
||||||
|
parentSpanId?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Event {
|
||||||
|
id: string;
|
||||||
|
type: string;
|
||||||
|
name: string;
|
||||||
|
timestamp: string;
|
||||||
|
metadata: Record<string, unknown>;
|
||||||
|
spanId?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Trace {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
status: TraceStatus;
|
||||||
|
startedAt: string;
|
||||||
|
endedAt: string | null;
|
||||||
|
durationMs: number | null;
|
||||||
|
tags: string[];
|
||||||
|
metadata: Record<string, unknown>;
|
||||||
|
costUsd: number | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DecisionTreeProps {
|
||||||
|
trace: Trace;
|
||||||
|
decisionPoints: DecisionPoint[];
|
||||||
|
spans: Span[];
|
||||||
|
events: Event[];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Color configurations
|
||||||
|
const spanTypeColors: Record<string, { bg: string; text: string; border: string }> = {
|
||||||
|
LLM_CALL: {
|
||||||
|
bg: "bg-purple-500/10",
|
||||||
|
text: "text-purple-400",
|
||||||
|
border: "border-purple-500/20",
|
||||||
|
},
|
||||||
|
TOOL_CALL: {
|
||||||
|
bg: "bg-blue-500/10",
|
||||||
|
text: "text-blue-400",
|
||||||
|
border: "border-blue-500/20",
|
||||||
|
},
|
||||||
|
CHAIN: {
|
||||||
|
bg: "bg-amber-500/10",
|
||||||
|
text: "text-amber-400",
|
||||||
|
border: "border-amber-500/20",
|
||||||
|
},
|
||||||
|
AGENT: {
|
||||||
|
bg: "bg-cyan-500/10",
|
||||||
|
text: "text-cyan-400",
|
||||||
|
border: "border-cyan-500/20",
|
||||||
|
},
|
||||||
|
MEMORY_OP: {
|
||||||
|
bg: "bg-pink-500/10",
|
||||||
|
text: "text-pink-400",
|
||||||
|
border: "border-pink-500/20",
|
||||||
|
},
|
||||||
|
CUSTOM: {
|
||||||
|
bg: "bg-neutral-700/30",
|
||||||
|
text: "text-neutral-400",
|
||||||
|
border: "border-neutral-600/30",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const spanStatusColors: Record<string, { dot: string; pulse?: boolean }> = {
|
||||||
|
OK: { dot: "bg-emerald-400" },
|
||||||
|
ERROR: { dot: "bg-red-400" },
|
||||||
|
CANCELLED: { dot: "bg-amber-400", pulse: true },
|
||||||
|
};
|
||||||
|
|
||||||
|
const decisionTypeColors: Record<string, { bg: string; text: string; border: string }> = {
|
||||||
|
TOOL_SELECTION: {
|
||||||
|
bg: "bg-blue-500/10",
|
||||||
|
text: "text-blue-400",
|
||||||
|
border: "border-blue-500/20",
|
||||||
|
},
|
||||||
|
PATH_SELECTION: {
|
||||||
|
bg: "bg-purple-500/10",
|
||||||
|
text: "text-purple-400",
|
||||||
|
border: "border-purple-500/20",
|
||||||
|
},
|
||||||
|
RESPONSE_FORMULATION: {
|
||||||
|
bg: "bg-emerald-500/10",
|
||||||
|
text: "text-emerald-400",
|
||||||
|
border: "border-emerald-500/20",
|
||||||
|
},
|
||||||
|
DEFAULT: {
|
||||||
|
bg: "bg-neutral-700/30",
|
||||||
|
text: "text-neutral-400",
|
||||||
|
border: "border-neutral-600/30",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const eventTypeIcons: Record<string, React.ComponentType<{ className?: string }>> = {
|
||||||
|
LLM_CALL: Zap,
|
||||||
|
TOOL_CALL: Terminal,
|
||||||
|
ERROR: AlertCircle,
|
||||||
|
DEFAULT: Activity,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Node data types
|
||||||
|
type TraceNodeData = {
|
||||||
|
trace: Trace;
|
||||||
|
} & Record<string, unknown>;
|
||||||
|
|
||||||
|
type SpanNodeData = {
|
||||||
|
span: Span;
|
||||||
|
maxDuration: number;
|
||||||
|
} & Record<string, unknown>;
|
||||||
|
|
||||||
|
type DecisionNodeData = {
|
||||||
|
decision: DecisionPoint;
|
||||||
|
} & Record<string, unknown>;
|
||||||
|
|
||||||
|
type EventNodeData = {
|
||||||
|
event: Event;
|
||||||
|
} & Record<string, unknown>;
|
||||||
|
|
||||||
|
// Custom Node Components
|
||||||
|
function TraceNode({ data, selected }: { data: TraceNodeData; selected?: boolean }) {
|
||||||
|
const { trace } = data;
|
||||||
|
|
||||||
|
const statusConfig: Record<TraceStatus, { icon: React.ComponentType<{ className?: string }>; color: string }> = {
|
||||||
|
RUNNING: { icon: Activity, color: "text-amber-400" },
|
||||||
|
COMPLETED: { icon: CheckCircle, color: "text-emerald-400" },
|
||||||
|
ERROR: { icon: XCircle, color: "text-red-400" },
|
||||||
|
};
|
||||||
|
|
||||||
|
const status = statusConfig[trace.status];
|
||||||
|
const StatusIcon = status.icon;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"w-[300px] p-4 rounded-xl bg-neutral-900 border-2 transition-all",
|
||||||
|
selected
|
||||||
|
? "border-emerald-500 shadow-[0_0_20px_rgba(16,185,129,0.3)]"
|
||||||
|
: "border-emerald-500/50 hover:border-emerald-400/70"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Handle type="target" position={Position.Top} className="!bg-emerald-500" />
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className="p-2 rounded-lg bg-emerald-500/10">
|
||||||
|
<GitBranch className="w-5 h-5 text-emerald-400" />
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<h3 className="font-semibold text-neutral-100 truncate">{trace.name}</h3>
|
||||||
|
<div className="flex items-center gap-2 mt-1">
|
||||||
|
<StatusIcon className={cn("w-3 h-3", status.color)} />
|
||||||
|
<span className={cn("text-xs", status.color)}>{trace.status}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2 mt-3 pt-3 border-t border-neutral-700">
|
||||||
|
<Clock className="w-3 h-3 text-neutral-500" />
|
||||||
|
<span className="text-xs text-neutral-400">{formatDuration(trace.durationMs)}</span>
|
||||||
|
</div>
|
||||||
|
<Handle type="source" position={Position.Bottom} className="!bg-emerald-500" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function SpanNode({ data, selected }: { data: SpanNodeData; selected?: boolean }) {
|
||||||
|
const { span, maxDuration } = data;
|
||||||
|
const colors = spanTypeColors[span.type] || spanTypeColors.CUSTOM;
|
||||||
|
const statusColor = spanStatusColors[span.status] || spanStatusColors.CANCELLED;
|
||||||
|
const durationPercent = maxDuration > 0 ? ((span.durationMs || 0) / maxDuration) * 100 : 0;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"w-[260px] p-3 rounded-xl bg-neutral-900 border transition-all",
|
||||||
|
selected
|
||||||
|
? "border-emerald-500 shadow-[0_0_20px_rgba(16,185,129,0.3)]"
|
||||||
|
: "border-neutral-700 hover:border-neutral-600"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Handle type="target" position={Position.Top} className="!bg-neutral-500" />
|
||||||
|
<div className="flex items-start gap-2">
|
||||||
|
<div className={cn("w-2 h-2 rounded-full mt-1.5", statusColor.dot, statusColor.pulse && "animate-pulse")} />
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<h4 className="font-medium text-neutral-100 text-sm truncate">{span.name}</h4>
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"inline-flex items-center px-1.5 py-0.5 rounded text-[10px] font-medium mt-1 border",
|
||||||
|
colors.bg,
|
||||||
|
colors.text,
|
||||||
|
colors.border
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{span.type}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="mt-2">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div className="flex-1 h-1.5 bg-neutral-800 rounded-full overflow-hidden">
|
||||||
|
<div
|
||||||
|
className="h-full bg-emerald-500 rounded-full transition-all"
|
||||||
|
style={{ width: `${durationPercent}%` }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<span className="text-xs text-neutral-400">{formatDuration(span.durationMs)}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Handle type="source" position={Position.Bottom} className="!bg-neutral-500" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function DecisionNode({ data, selected }: { data: DecisionNodeData; selected?: boolean }) {
|
||||||
|
const { decision } = data;
|
||||||
|
const [expanded, setExpanded] = useState(false);
|
||||||
|
const colors = decisionTypeColors[decision.type] || decisionTypeColors.DEFAULT;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"w-[240px] rounded-xl bg-neutral-900 border transition-all overflow-hidden",
|
||||||
|
selected
|
||||||
|
? "border-emerald-500 shadow-[0_0_20px_rgba(16,185,129,0.3)]"
|
||||||
|
: "border-neutral-700 hover:border-neutral-600"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Handle type="target" position={Position.Top} className="!bg-neutral-500" />
|
||||||
|
<div className="p-3">
|
||||||
|
<div className="flex items-center gap-2 mb-2">
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"px-1.5 py-0.5 rounded text-[10px] font-medium border",
|
||||||
|
colors.bg,
|
||||||
|
colors.text,
|
||||||
|
colors.border
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{decision.type}
|
||||||
|
</div>
|
||||||
|
{decision.confidence !== null && (
|
||||||
|
<span className="text-[10px] text-emerald-400 font-medium">
|
||||||
|
{Math.round(decision.confidence * 100)}%
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<p className="text-sm text-neutral-100 truncate">{decision.chosenAction}</p>
|
||||||
|
{decision.alternatives.length > 0 && (
|
||||||
|
<button
|
||||||
|
onClick={() => setExpanded(!expanded)}
|
||||||
|
className="flex items-center gap-1 mt-2 text-[10px] text-neutral-400 hover:text-neutral-200 transition-colors"
|
||||||
|
>
|
||||||
|
{expanded ? (
|
||||||
|
<ChevronDown className="w-3 h-3" />
|
||||||
|
) : (
|
||||||
|
<ChevronRight className="w-3 h-3" />
|
||||||
|
)}
|
||||||
|
{decision.alternatives.length} alternative{decision.alternatives.length !== 1 ? "s" : ""}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
{expanded && decision.alternatives.length > 0 && (
|
||||||
|
<div className="mt-2 space-y-1">
|
||||||
|
{decision.alternatives.map((alt, idx) => (
|
||||||
|
<div
|
||||||
|
key={idx}
|
||||||
|
className="flex items-center gap-1 text-[10px] text-neutral-500 px-2 py-1 bg-neutral-800/50 rounded"
|
||||||
|
>
|
||||||
|
<span className="text-neutral-600">#{idx + 1}</span>
|
||||||
|
<span className="truncate">{alt}</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{decision.reasoning && (
|
||||||
|
<div className="mt-2 pt-2 border-t border-neutral-800">
|
||||||
|
<div className="flex items-center gap-1 text-[10px] text-neutral-500">
|
||||||
|
<Lightbulb className="w-3 h-3" />
|
||||||
|
<span className="truncate">{decision.reasoning}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<Handle type="source" position={Position.Bottom} className="!bg-neutral-500" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function EventNode({ data, selected }: { data: EventNodeData; selected?: boolean }) {
|
||||||
|
const { event } = data;
|
||||||
|
const Icon = eventTypeIcons[event.type] || eventTypeIcons.DEFAULT;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"w-[180px] p-2 rounded-xl bg-neutral-900 border transition-all",
|
||||||
|
selected
|
||||||
|
? "border-emerald-500 shadow-[0_0_20px_rgba(16,185,129,0.3)]"
|
||||||
|
: "border-neutral-700 hover:border-neutral-600"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Handle type="target" position={Position.Top} className="!bg-neutral-500" />
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div className="p-1.5 rounded-lg bg-neutral-800">
|
||||||
|
<Icon className="w-3 h-3 text-neutral-400" />
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<p className="text-xs text-neutral-100 truncate">{event.name}</p>
|
||||||
|
<p className="text-[10px] text-neutral-500">{event.type}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Handle type="source" position={Position.Bottom} className="!bg-neutral-500" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Node types must be defined outside the component to avoid re-renders
|
||||||
|
const nodeTypes = {
|
||||||
|
trace: TraceNode,
|
||||||
|
span: SpanNode,
|
||||||
|
decision: DecisionNode,
|
||||||
|
event: EventNode,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Build the tree layout using Dagre
|
||||||
|
function buildTreeLayout(
|
||||||
|
trace: Trace,
|
||||||
|
spans: Span[],
|
||||||
|
decisionPoints: DecisionPoint[],
|
||||||
|
events: Event[]
|
||||||
|
): { nodes: Node[]; edges: Edge[] } {
|
||||||
|
const g = new dagre.graphlib.Graph();
|
||||||
|
g.setDefaultEdgeLabel(() => ({}));
|
||||||
|
g.setGraph({
|
||||||
|
rankdir: "TB",
|
||||||
|
ranksep: 80,
|
||||||
|
nodesep: 40,
|
||||||
|
});
|
||||||
|
|
||||||
|
const nodes: Node[] = [];
|
||||||
|
const edges: Edge[] = [];
|
||||||
|
|
||||||
|
// Add root trace node
|
||||||
|
const traceNodeId = `trace-${trace.id}`;
|
||||||
|
g.setNode(traceNodeId, { width: 300, height: 100 });
|
||||||
|
nodes.push({
|
||||||
|
id: traceNodeId,
|
||||||
|
type: "trace",
|
||||||
|
data: { trace } as TraceNodeData,
|
||||||
|
position: { x: 0, y: 0 },
|
||||||
|
});
|
||||||
|
|
||||||
|
// Calculate max duration for span bars
|
||||||
|
const maxDuration = Math.max(...spans.map((s) => s.durationMs || 0), 1);
|
||||||
|
|
||||||
|
// Add span nodes and build parent-child relationships
|
||||||
|
const spanMap = new Map<string, string>(); // spanId -> nodeId
|
||||||
|
const childSpans = new Map<string, string[]>(); // parentSpanId -> spanIds
|
||||||
|
|
||||||
|
spans.forEach((span) => {
|
||||||
|
const nodeId = `span-${span.id}`;
|
||||||
|
spanMap.set(span.id, nodeId);
|
||||||
|
|
||||||
|
if (span.parentSpanId) {
|
||||||
|
const siblings = childSpans.get(span.parentSpanId) || [];
|
||||||
|
siblings.push(span.id);
|
||||||
|
childSpans.set(span.parentSpanId, siblings);
|
||||||
|
}
|
||||||
|
|
||||||
|
g.setNode(nodeId, { width: 260, height: 80 });
|
||||||
|
nodes.push({
|
||||||
|
id: nodeId,
|
||||||
|
type: "span",
|
||||||
|
data: { span, maxDuration } as SpanNodeData,
|
||||||
|
position: { x: 0, y: 0 },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Connect spans to their parents or to trace root
|
||||||
|
spans.forEach((span) => {
|
||||||
|
const nodeId = spanMap.get(span.id)!;
|
||||||
|
if (span.parentSpanId && spanMap.has(span.parentSpanId)) {
|
||||||
|
const parentId = spanMap.get(span.parentSpanId)!;
|
||||||
|
g.setEdge(parentId, nodeId);
|
||||||
|
edges.push({
|
||||||
|
id: `e-${parentId}-${nodeId}`,
|
||||||
|
source: parentId,
|
||||||
|
target: nodeId,
|
||||||
|
type: ConnectionLineType.SmoothStep,
|
||||||
|
animated: true,
|
||||||
|
style: { stroke: span.status === "ERROR" ? "#f87171" : "#34d399" },
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Top-level span - connect to trace root
|
||||||
|
g.setEdge(traceNodeId, nodeId);
|
||||||
|
edges.push({
|
||||||
|
id: `e-${traceNodeId}-${nodeId}`,
|
||||||
|
source: traceNodeId,
|
||||||
|
target: nodeId,
|
||||||
|
type: ConnectionLineType.SmoothStep,
|
||||||
|
animated: true,
|
||||||
|
style: { stroke: span.status === "ERROR" ? "#f87171" : "#34d399" },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add decision nodes
|
||||||
|
decisionPoints.forEach((decision) => {
|
||||||
|
const nodeId = `decision-${decision.id}`;
|
||||||
|
g.setNode(nodeId, { width: 240, height: 90 });
|
||||||
|
nodes.push({
|
||||||
|
id: nodeId,
|
||||||
|
type: "decision",
|
||||||
|
data: { decision } as DecisionNodeData,
|
||||||
|
position: { x: 0, y: 0 },
|
||||||
|
});
|
||||||
|
|
||||||
|
// Connect to parent span or trace root
|
||||||
|
if (decision.parentSpanId && spanMap.has(decision.parentSpanId)) {
|
||||||
|
const parentId = spanMap.get(decision.parentSpanId)!;
|
||||||
|
g.setEdge(parentId, nodeId);
|
||||||
|
edges.push({
|
||||||
|
id: `e-${parentId}-${nodeId}`,
|
||||||
|
source: parentId,
|
||||||
|
target: nodeId,
|
||||||
|
type: ConnectionLineType.SmoothStep,
|
||||||
|
animated: true,
|
||||||
|
style: { stroke: "#60a5fa" },
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
g.setEdge(traceNodeId, nodeId);
|
||||||
|
edges.push({
|
||||||
|
id: `e-${traceNodeId}-${nodeId}`,
|
||||||
|
source: traceNodeId,
|
||||||
|
target: nodeId,
|
||||||
|
type: ConnectionLineType.SmoothStep,
|
||||||
|
animated: true,
|
||||||
|
style: { stroke: "#60a5fa" },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add event nodes
|
||||||
|
events.forEach((event) => {
|
||||||
|
const nodeId = `event-${event.id}`;
|
||||||
|
g.setNode(nodeId, { width: 180, height: 50 });
|
||||||
|
nodes.push({
|
||||||
|
id: nodeId,
|
||||||
|
type: "event",
|
||||||
|
data: { event } as EventNodeData,
|
||||||
|
position: { x: 0, y: 0 },
|
||||||
|
});
|
||||||
|
|
||||||
|
// Connect to span or trace root
|
||||||
|
if (event.spanId && spanMap.has(event.spanId)) {
|
||||||
|
const parentId = spanMap.get(event.spanId)!;
|
||||||
|
g.setEdge(parentId, nodeId);
|
||||||
|
edges.push({
|
||||||
|
id: `e-${parentId}-${nodeId}`,
|
||||||
|
source: parentId,
|
||||||
|
target: nodeId,
|
||||||
|
type: ConnectionLineType.SmoothStep,
|
||||||
|
animated: true,
|
||||||
|
style: { stroke: "#a78bfa" },
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
g.setEdge(traceNodeId, nodeId);
|
||||||
|
edges.push({
|
||||||
|
id: `e-${traceNodeId}-${nodeId}`,
|
||||||
|
source: traceNodeId,
|
||||||
|
target: nodeId,
|
||||||
|
type: ConnectionLineType.SmoothStep,
|
||||||
|
animated: true,
|
||||||
|
style: { stroke: "#a78bfa" },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Calculate layout
|
||||||
|
dagre.layout(g);
|
||||||
|
|
||||||
|
// Update node positions from Dagre
|
||||||
|
nodes.forEach((node) => {
|
||||||
|
const dagreNode = g.node(node.id);
|
||||||
|
node.position = {
|
||||||
|
x: dagreNode.x - dagreNode.width / 2,
|
||||||
|
y: dagreNode.y - dagreNode.height / 2,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return { nodes, edges };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function DecisionTree({ trace, spans, decisionPoints, events }: DecisionTreeProps) {
|
||||||
|
const { nodes: initialNodes, edges: initialEdges } = useMemo(
|
||||||
|
() => buildTreeLayout(trace, spans, decisionPoints, events),
|
||||||
|
[trace, spans, decisionPoints, events]
|
||||||
|
);
|
||||||
|
|
||||||
|
const [nodes, _, onNodesChange] = useNodesState(initialNodes);
|
||||||
|
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
|
||||||
|
const [selectedNode, setSelectedNode] = useState<Node | null>(null);
|
||||||
|
|
||||||
|
const onConnect = useCallback(
|
||||||
|
(params: Connection) => {
|
||||||
|
setEdges((eds) => addEdge(params, eds));
|
||||||
|
},
|
||||||
|
[setEdges]
|
||||||
|
);
|
||||||
|
|
||||||
|
const onNodeClick = useCallback((_: React.MouseEvent, node: Node) => {
|
||||||
|
setSelectedNode((prev) => (prev?.id === node.id ? null : node));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Empty state
|
||||||
|
if (spans.length === 0 && decisionPoints.length === 0 && events.length === 0) {
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col items-center justify-center min-h-[600px] text-center">
|
||||||
|
<Layers className="w-12 h-12 text-neutral-600 mb-4" />
|
||||||
|
<h3 className="text-lg font-medium text-neutral-300 mb-2">No trace data available</h3>
|
||||||
|
<p className="text-sm text-neutral-500 max-w-md">
|
||||||
|
This trace does not contain any spans, decisions, or events to visualize.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex gap-4 h-[600px]">
|
||||||
|
<div className="flex-1 rounded-xl border border-neutral-800 overflow-hidden bg-neutral-950">
|
||||||
|
<ReactFlow
|
||||||
|
nodes={nodes}
|
||||||
|
edges={edges}
|
||||||
|
onNodesChange={onNodesChange}
|
||||||
|
onEdgesChange={onEdgesChange}
|
||||||
|
onConnect={onConnect}
|
||||||
|
onNodeClick={onNodeClick}
|
||||||
|
nodeTypes={nodeTypes}
|
||||||
|
connectionLineType={ConnectionLineType.SmoothStep}
|
||||||
|
fitView
|
||||||
|
minZoom={0.1}
|
||||||
|
maxZoom={2}
|
||||||
|
colorMode="dark"
|
||||||
|
attributionPosition="bottom-right"
|
||||||
|
>
|
||||||
|
<Background variant={BackgroundVariant.Dots} gap={20} size={1} color="#404040" />
|
||||||
|
<MiniMap
|
||||||
|
nodeColor={(node) => {
|
||||||
|
switch (node.type) {
|
||||||
|
case "trace":
|
||||||
|
return "#10b981";
|
||||||
|
case "span":
|
||||||
|
return "#3b82f6";
|
||||||
|
case "decision":
|
||||||
|
return "#8b5cf6";
|
||||||
|
case "event":
|
||||||
|
return "#f59e0b";
|
||||||
|
default:
|
||||||
|
return "#6b7280";
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
maskColor="rgba(23, 23, 23, 0.8)"
|
||||||
|
className="!bg-neutral-900/80 !border-neutral-700"
|
||||||
|
/>
|
||||||
|
<Controls className="!bg-neutral-900 !border-neutral-700 [&>button]:!bg-neutral-800 [&>button]:!border-neutral-700 [&>button]:!text-neutral-300 [&>button:hover]:!bg-neutral-700" />
|
||||||
|
</ReactFlow>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Details Panel */}
|
||||||
|
{selectedNode && (
|
||||||
|
<div className="w-80 rounded-xl border border-neutral-800 bg-neutral-900 p-4 overflow-y-auto">
|
||||||
|
<div className="flex items-center justify-between mb-4">
|
||||||
|
<h3 className="font-semibold text-neutral-100">Details</h3>
|
||||||
|
<button
|
||||||
|
onClick={() => setSelectedNode(null)}
|
||||||
|
className="text-neutral-400 hover:text-neutral-200 transition-colors"
|
||||||
|
>
|
||||||
|
<XCircle className="w-5 h-5" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<NodeDetails node={selectedNode} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function NodeDetails({ node }: { node: Node }) {
|
||||||
|
switch (node.type) {
|
||||||
|
case "trace": {
|
||||||
|
const data = node.data as unknown as TraceNodeData;
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<span className="text-xs text-neutral-500 uppercase tracking-wide">Name</span>
|
||||||
|
<p className="text-sm text-neutral-100 mt-1">{data.trace.name}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="text-xs text-neutral-500 uppercase tracking-wide">Status</span>
|
||||||
|
<p className="text-sm text-neutral-100 mt-1">{data.trace.status}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="text-xs text-neutral-500 uppercase tracking-wide">Duration</span>
|
||||||
|
<p className="text-sm text-neutral-100 mt-1">{formatDuration(data.trace.durationMs)}</p>
|
||||||
|
</div>
|
||||||
|
{data.trace.costUsd !== null && (
|
||||||
|
<div>
|
||||||
|
<span className="text-xs text-neutral-500 uppercase tracking-wide">Cost</span>
|
||||||
|
<p className="text-sm text-neutral-100 mt-1">${data.trace.costUsd.toFixed(4)}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{data.trace.tags.length > 0 && (
|
||||||
|
<div>
|
||||||
|
<span className="text-xs text-neutral-500 uppercase tracking-wide">Tags</span>
|
||||||
|
<div className="flex flex-wrap gap-2 mt-1">
|
||||||
|
{data.trace.tags.map((tag) => (
|
||||||
|
<span key={tag} className="px-2 py-1 rounded bg-neutral-800 text-xs text-neutral-300">
|
||||||
|
{tag}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
case "span": {
|
||||||
|
const data = node.data as unknown as SpanNodeData;
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<span className="text-xs text-neutral-500 uppercase tracking-wide">Name</span>
|
||||||
|
<p className="text-sm text-neutral-100 mt-1">{data.span.name}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="text-xs text-neutral-500 uppercase tracking-wide">Type</span>
|
||||||
|
<p className="text-sm text-neutral-100 mt-1">{data.span.type}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="text-xs text-neutral-500 uppercase tracking-wide">Status</span>
|
||||||
|
<p className="text-sm text-neutral-100 mt-1">{data.span.status}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="text-xs text-neutral-500 uppercase tracking-wide">Duration</span>
|
||||||
|
<p className="text-sm text-neutral-100 mt-1">{formatDuration(data.span.durationMs)}</p>
|
||||||
|
</div>
|
||||||
|
{Boolean(data.span.input) && (
|
||||||
|
<div>
|
||||||
|
<span className="text-xs text-neutral-500 uppercase tracking-wide">Input</span>
|
||||||
|
<pre className="mt-1 p-2 bg-neutral-950 rounded text-xs text-neutral-300 overflow-x-auto">
|
||||||
|
{JSON.stringify(data.span.input, null, 2)}
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{Boolean(data.span.output) && (
|
||||||
|
<div>
|
||||||
|
<span className="text-xs text-neutral-500 uppercase tracking-wide">Output</span>
|
||||||
|
<pre className="mt-1 p-2 bg-neutral-950 rounded text-xs text-neutral-300 overflow-x-auto">
|
||||||
|
{JSON.stringify(data.span.output, null, 2)}
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
case "decision": {
|
||||||
|
const data = node.data as unknown as DecisionNodeData;
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<span className="text-xs text-neutral-500 uppercase tracking-wide">Type</span>
|
||||||
|
<p className="text-sm text-neutral-100 mt-1">{data.decision.type}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="text-xs text-neutral-500 uppercase tracking-wide">Chosen Action</span>
|
||||||
|
<p className="text-sm text-neutral-100 mt-1">{data.decision.chosenAction}</p>
|
||||||
|
</div>
|
||||||
|
{data.decision.confidence !== null && (
|
||||||
|
<div>
|
||||||
|
<span className="text-xs text-neutral-500 uppercase tracking-wide">Confidence</span>
|
||||||
|
<p className="text-sm text-emerald-400 mt-1">{Math.round(data.decision.confidence * 100)}%</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{data.decision.alternatives.length > 0 && (
|
||||||
|
<div>
|
||||||
|
<span className="text-xs text-neutral-500 uppercase tracking-wide">
|
||||||
|
Alternatives ({data.decision.alternatives.length})
|
||||||
|
</span>
|
||||||
|
<div className="space-y-1 mt-1">
|
||||||
|
{data.decision.alternatives.map((alt, idx) => (
|
||||||
|
<p key={idx} className="text-xs text-neutral-400">
|
||||||
|
{idx + 1}. {alt}
|
||||||
|
</p>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{data.decision.reasoning && (
|
||||||
|
<div>
|
||||||
|
<span className="text-xs text-neutral-500 uppercase tracking-wide">Reasoning</span>
|
||||||
|
<p className="text-sm text-neutral-300 mt-1 italic">“{data.decision.reasoning}”</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
case "event": {
|
||||||
|
const data = node.data as unknown as EventNodeData;
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<span className="text-xs text-neutral-500 uppercase tracking-wide">Name</span>
|
||||||
|
<p className="text-sm text-neutral-100 mt-1">{data.event.name}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="text-xs text-neutral-500 uppercase tracking-wide">Type</span>
|
||||||
|
<p className="text-sm text-neutral-100 mt-1">{data.event.type}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="text-xs text-neutral-500 uppercase tracking-wide">Timestamp</span>
|
||||||
|
<p className="text-sm text-neutral-100 mt-1">{new Date(data.event.timestamp).toLocaleString()}</p>
|
||||||
|
</div>
|
||||||
|
{Object.keys(data.event.metadata).length > 0 && (
|
||||||
|
<div>
|
||||||
|
<span className="text-xs text-neutral-500 uppercase tracking-wide">Metadata</span>
|
||||||
|
<pre className="mt-1 p-2 bg-neutral-950 rounded text-xs text-neutral-300 overflow-x-auto">
|
||||||
|
{JSON.stringify(data.event.metadata, null, 2)}
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -20,9 +20,10 @@ import {
|
|||||||
Terminal,
|
Terminal,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { cn, formatDuration, formatRelativeTime } from "@/lib/utils";
|
import { cn, formatDuration, formatRelativeTime } from "@/lib/utils";
|
||||||
|
import { DecisionTree } from "./decision-tree";
|
||||||
|
|
||||||
type TraceStatus = "RUNNING" | "COMPLETED" | "ERROR";
|
type TraceStatus = "RUNNING" | "COMPLETED" | "ERROR";
|
||||||
type TabType = "decisions" | "spans" | "events";
|
type TabType = "tree" | "decisions" | "spans" | "events";
|
||||||
|
|
||||||
interface DecisionPoint {
|
interface DecisionPoint {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -33,6 +34,7 @@ interface DecisionPoint {
|
|||||||
contextSnapshot: Record<string, unknown> | null;
|
contextSnapshot: Record<string, unknown> | null;
|
||||||
confidence: number | null;
|
confidence: number | null;
|
||||||
timestamp: string;
|
timestamp: string;
|
||||||
|
parentSpanId?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Span {
|
interface Span {
|
||||||
@@ -46,6 +48,7 @@ interface Span {
|
|||||||
input: unknown;
|
input: unknown;
|
||||||
output: unknown;
|
output: unknown;
|
||||||
metadata: Record<string, unknown>;
|
metadata: Record<string, unknown>;
|
||||||
|
parentSpanId?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Event {
|
interface Event {
|
||||||
@@ -54,6 +57,7 @@ interface Event {
|
|||||||
name: string;
|
name: string;
|
||||||
timestamp: string;
|
timestamp: string;
|
||||||
metadata: Record<string, unknown>;
|
metadata: Record<string, unknown>;
|
||||||
|
spanId?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Trace {
|
interface Trace {
|
||||||
@@ -138,7 +142,7 @@ export function TraceDetail({
|
|||||||
spans,
|
spans,
|
||||||
events,
|
events,
|
||||||
}: TraceDetailProps) {
|
}: TraceDetailProps) {
|
||||||
const [activeTab, setActiveTab] = useState<TabType>("decisions");
|
const [activeTab, setActiveTab] = useState<TabType>("tree");
|
||||||
const status = statusConfig[trace.status];
|
const status = statusConfig[trace.status];
|
||||||
const StatusIcon = status.icon;
|
const StatusIcon = status.icon;
|
||||||
|
|
||||||
@@ -243,6 +247,12 @@ export function TraceDetail({
|
|||||||
{/* Tabs */}
|
{/* Tabs */}
|
||||||
<div className="border-b border-neutral-800">
|
<div className="border-b border-neutral-800">
|
||||||
<div className="flex gap-1">
|
<div className="flex gap-1">
|
||||||
|
<TabButton
|
||||||
|
active={activeTab === "tree"}
|
||||||
|
onClick={() => setActiveTab("tree")}
|
||||||
|
icon={GitBranch}
|
||||||
|
label="Tree"
|
||||||
|
/>
|
||||||
<TabButton
|
<TabButton
|
||||||
active={activeTab === "decisions"}
|
active={activeTab === "decisions"}
|
||||||
onClick={() => setActiveTab("decisions")}
|
onClick={() => setActiveTab("decisions")}
|
||||||
@@ -265,7 +275,15 @@ export function TraceDetail({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Tab Content */}
|
{/* Tab Content */}
|
||||||
<div className="min-h-[400px]">
|
<div className="min-h-[600px]">
|
||||||
|
{activeTab === "tree" && (
|
||||||
|
<DecisionTree
|
||||||
|
trace={trace}
|
||||||
|
spans={spans}
|
||||||
|
decisionPoints={decisionPoints}
|
||||||
|
events={events}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
{activeTab === "decisions" && (
|
{activeTab === "decisions" && (
|
||||||
<DecisionsTab decisionPoints={decisionPoints} />
|
<DecisionsTab decisionPoints={decisionPoints} />
|
||||||
)}
|
)}
|
||||||
|
|||||||
258
package-lock.json
generated
258
package-lock.json
generated
@@ -22,6 +22,8 @@
|
|||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@agentlens/database": "*",
|
"@agentlens/database": "*",
|
||||||
|
"@dagrejs/dagre": "^2.0.4",
|
||||||
|
"@xyflow/react": "^12.10.0",
|
||||||
"lucide-react": "^0.469.0",
|
"lucide-react": "^0.469.0",
|
||||||
"next": "^15.1.0",
|
"next": "^15.1.0",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
@@ -29,6 +31,7 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tailwindcss/postcss": "^4.0.0",
|
"@tailwindcss/postcss": "^4.0.0",
|
||||||
|
"@types/dagre": "^0.7.53",
|
||||||
"@types/node": "^22.0.0",
|
"@types/node": "^22.0.0",
|
||||||
"@types/react": "^19.0.0",
|
"@types/react": "^19.0.0",
|
||||||
"@types/react-dom": "^19.0.0",
|
"@types/react-dom": "^19.0.0",
|
||||||
@@ -58,6 +61,21 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@dagrejs/dagre": {
|
||||||
|
"version": "2.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@dagrejs/dagre/-/dagre-2.0.4.tgz",
|
||||||
|
"integrity": "sha512-J6vCWTNpicHF4zFlZG1cS5DkGzMr9941gddYkakjrg3ZNev4bbqEgLHFTWiFrcJm7UCRu7olO3K6IRDd9gSGhA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@dagrejs/graphlib": "3.0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@dagrejs/graphlib": {
|
||||||
|
"version": "3.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@dagrejs/graphlib/-/graphlib-3.0.4.tgz",
|
||||||
|
"integrity": "sha512-HxZ7fCvAwTLCWCO0WjDkzAFQze8LdC6iOpKbetDKHIuDfIgMlIzYzqZ4nxwLlclQX+3ZVeZ1K2OuaOE2WWcyOg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@emnapi/runtime": {
|
"node_modules/@emnapi/runtime": {
|
||||||
"version": "1.8.1",
|
"version": "1.8.1",
|
||||||
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.8.1.tgz",
|
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.8.1.tgz",
|
||||||
@@ -1090,6 +1108,62 @@
|
|||||||
"tailwindcss": "4.1.18"
|
"tailwindcss": "4.1.18"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/d3-color": {
|
||||||
|
"version": "3.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz",
|
||||||
|
"integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@types/d3-drag": {
|
||||||
|
"version": "3.0.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/d3-drag/-/d3-drag-3.0.7.tgz",
|
||||||
|
"integrity": "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/d3-selection": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/d3-interpolate": {
|
||||||
|
"version": "3.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz",
|
||||||
|
"integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/d3-color": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/d3-selection": {
|
||||||
|
"version": "3.0.11",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.11.tgz",
|
||||||
|
"integrity": "sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@types/d3-transition": {
|
||||||
|
"version": "3.0.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/d3-transition/-/d3-transition-3.0.9.tgz",
|
||||||
|
"integrity": "sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/d3-selection": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/d3-zoom": {
|
||||||
|
"version": "3.0.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-3.0.8.tgz",
|
||||||
|
"integrity": "sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/d3-interpolate": "*",
|
||||||
|
"@types/d3-selection": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/dagre": {
|
||||||
|
"version": "0.7.53",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/dagre/-/dagre-0.7.53.tgz",
|
||||||
|
"integrity": "sha512-f4gkWqzPZvYmKhOsDnhq/R8mO4UMcKdxZo+i5SCkOU1wvGeHJeUXGIHeE9pnwGyPMDof1Vx5ZQo4nxpeg2TTVQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@types/node": {
|
"node_modules/@types/node": {
|
||||||
"version": "22.19.10",
|
"version": "22.19.10",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.10.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.10.tgz",
|
||||||
@@ -1104,7 +1178,7 @@
|
|||||||
"version": "19.2.13",
|
"version": "19.2.13",
|
||||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.13.tgz",
|
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.13.tgz",
|
||||||
"integrity": "sha512-KkiJeU6VbYbUOp5ITMIc7kBfqlYkKA5KhEHVrGMmUUMt7NeaZg65ojdPk+FtNrBAOXNVM5QM72jnADjM+XVRAQ==",
|
"integrity": "sha512-KkiJeU6VbYbUOp5ITMIc7kBfqlYkKA5KhEHVrGMmUUMt7NeaZg65ojdPk+FtNrBAOXNVM5QM72jnADjM+XVRAQ==",
|
||||||
"dev": true,
|
"devOptional": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"csstype": "^3.2.2"
|
"csstype": "^3.2.2"
|
||||||
@@ -1120,6 +1194,38 @@
|
|||||||
"@types/react": "^19.2.0"
|
"@types/react": "^19.2.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@xyflow/react": {
|
||||||
|
"version": "12.10.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@xyflow/react/-/react-12.10.0.tgz",
|
||||||
|
"integrity": "sha512-eOtz3whDMWrB4KWVatIBrKuxECHqip6PfA8fTpaS2RUGVpiEAe+nqDKsLqkViVWxDGreq0lWX71Xth/SPAzXiw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@xyflow/system": "0.0.74",
|
||||||
|
"classcat": "^5.0.3",
|
||||||
|
"zustand": "^4.4.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">=17",
|
||||||
|
"react-dom": ">=17"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@xyflow/system": {
|
||||||
|
"version": "0.0.74",
|
||||||
|
"resolved": "https://registry.npmjs.org/@xyflow/system/-/system-0.0.74.tgz",
|
||||||
|
"integrity": "sha512-7v7B/PkiVrkdZzSbL+inGAo6tkR/WQHHG0/jhSvLQToCsfa8YubOGmBYd1s08tpKpihdHDZFwzQZeR69QSBb4Q==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/d3-drag": "^3.0.7",
|
||||||
|
"@types/d3-interpolate": "^3.0.4",
|
||||||
|
"@types/d3-selection": "^3.0.10",
|
||||||
|
"@types/d3-transition": "^3.0.8",
|
||||||
|
"@types/d3-zoom": "^3.0.8",
|
||||||
|
"d3-drag": "^3.0.0",
|
||||||
|
"d3-interpolate": "^3.0.1",
|
||||||
|
"d3-selection": "^3.0.0",
|
||||||
|
"d3-zoom": "^3.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/c12": {
|
"node_modules/c12": {
|
||||||
"version": "3.1.0",
|
"version": "3.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/c12/-/c12-3.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/c12/-/c12-3.1.0.tgz",
|
||||||
@@ -1195,6 +1301,12 @@
|
|||||||
"consola": "^3.2.3"
|
"consola": "^3.2.3"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/classcat": {
|
||||||
|
"version": "5.0.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/classcat/-/classcat-5.0.5.tgz",
|
||||||
|
"integrity": "sha512-JhZUT7JFcQy/EzW605k/ktHtncoo9vnyW/2GspNYwFlN1C/WmjuV/xtS04e9SOkL2sTdw0VAZ2UGCcQ9lR6p6w==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/client-only": {
|
"node_modules/client-only": {
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz",
|
||||||
@@ -1222,9 +1334,114 @@
|
|||||||
"version": "3.2.3",
|
"version": "3.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
|
||||||
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
|
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
|
||||||
"dev": true,
|
"devOptional": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/d3-color": {
|
||||||
|
"version": "3.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz",
|
||||||
|
"integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==",
|
||||||
|
"license": "ISC",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/d3-dispatch": {
|
||||||
|
"version": "3.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz",
|
||||||
|
"integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==",
|
||||||
|
"license": "ISC",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/d3-drag": {
|
||||||
|
"version": "3.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz",
|
||||||
|
"integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"d3-dispatch": "1 - 3",
|
||||||
|
"d3-selection": "3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/d3-ease": {
|
||||||
|
"version": "3.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz",
|
||||||
|
"integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==",
|
||||||
|
"license": "BSD-3-Clause",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/d3-interpolate": {
|
||||||
|
"version": "3.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz",
|
||||||
|
"integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"d3-color": "1 - 3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/d3-selection": {
|
||||||
|
"version": "3.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz",
|
||||||
|
"integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==",
|
||||||
|
"license": "ISC",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/d3-timer": {
|
||||||
|
"version": "3.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz",
|
||||||
|
"integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==",
|
||||||
|
"license": "ISC",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/d3-transition": {
|
||||||
|
"version": "3.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz",
|
||||||
|
"integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"d3-color": "1 - 3",
|
||||||
|
"d3-dispatch": "1 - 3",
|
||||||
|
"d3-ease": "1 - 3",
|
||||||
|
"d3-interpolate": "1 - 3",
|
||||||
|
"d3-timer": "1 - 3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"d3-selection": "2 - 3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/d3-zoom": {
|
||||||
|
"version": "3.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz",
|
||||||
|
"integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"d3-dispatch": "1 - 3",
|
||||||
|
"d3-drag": "2 - 3",
|
||||||
|
"d3-interpolate": "1 - 3",
|
||||||
|
"d3-selection": "2 - 3",
|
||||||
|
"d3-transition": "2 - 3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/deepmerge-ts": {
|
"node_modules/deepmerge-ts": {
|
||||||
"version": "7.1.5",
|
"version": "7.1.5",
|
||||||
"resolved": "https://registry.npmjs.org/deepmerge-ts/-/deepmerge-ts-7.1.5.tgz",
|
"resolved": "https://registry.npmjs.org/deepmerge-ts/-/deepmerge-ts-7.1.5.tgz",
|
||||||
@@ -2195,6 +2412,43 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/use-sync-external-store": {
|
||||||
|
"version": "1.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz",
|
||||||
|
"integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/zustand": {
|
||||||
|
"version": "4.5.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.7.tgz",
|
||||||
|
"integrity": "sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"use-sync-external-store": "^1.2.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12.7.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": ">=16.8",
|
||||||
|
"immer": ">=9.0.6",
|
||||||
|
"react": ">=16.8"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"immer": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"packages/database": {
|
"packages/database": {
|
||||||
"name": "@agentlens/database",
|
"name": "@agentlens/database",
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
|
|||||||
Reference in New Issue
Block a user