Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions src/components/chat/AgentMessage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import ReactMarkdown from 'react-markdown';
import remarkGfm from 'remark-gfm';
import { ChatMessage } from '@/types';
import { Copy, Check } from 'lucide-react';
import ExecutionTrace from './ExecutionTrace';

interface AgentMessageProps {
message: ChatMessage;
Expand Down Expand Up @@ -241,6 +242,12 @@ export default function AgentMessage({ message, onCopy }: AgentMessageProps) {
{renderContent()}
</ReactMarkdown>
</div>

{/* Execution Trace */}
{message.metadata?.executionTrace && (
<ExecutionTrace trace={message.metadata.executionTrace} />
)}

<button
onClick={handleCopy}
className="absolute top-0 right-0 opacity-0 group-hover:opacity-100 transition-opacity p-1 text-gray-400 hover:text-white"
Expand Down
184 changes: 184 additions & 0 deletions src/components/chat/ExecutionTrace.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
'use client';

import React, { useState } from 'react';
import type { ExecutionTrace, ExecutionStep } from '@/types';
import { ChevronDown, ChevronRight, Brain, Zap, Wrench, CheckCircle, XCircle, Clock, Cpu } from 'lucide-react';

interface ExecutionTraceProps {
trace: ExecutionTrace;
}

export default function ExecutionTrace({ trace }: ExecutionTraceProps) {
const [expandedSteps, setExpandedSteps] = useState<Set<string>>(new Set());
const [isExpanded, setIsExpanded] = useState(false);

const toggleStep = (stepId: string) => {
const newExpanded = new Set(expandedSteps);
if (newExpanded.has(stepId)) {
newExpanded.delete(stepId);
} else {
newExpanded.add(stepId);
}
setExpandedSteps(newExpanded);
};

const getStepIcon = (type: ExecutionStep['type']) => {
switch (type) {
case 'thought':
return <Brain className="h-4 w-4 text-purple-400" />;
case 'action':
return <Zap className="h-4 w-4 text-blue-400" />;
case 'tool_call':
return <Wrench className="h-4 w-4 text-orange-400" />;
case 'result':
return <CheckCircle className="h-4 w-4 text-green-400" />;
case 'error':
return <XCircle className="h-4 w-4 text-red-400" />;
default:
return <Cpu className="h-4 w-4 text-gray-400" />;
}
};

const getStepColor = (type: ExecutionStep['type']) => {
switch (type) {
case 'thought':
return 'border-purple-500/30 bg-purple-500/10';
case 'action':
return 'border-blue-500/30 bg-blue-500/10';
case 'tool_call':
return 'border-orange-500/30 bg-orange-500/10';
case 'result':
return 'border-green-500/30 bg-green-500/10';
case 'error':
return 'border-red-500/30 bg-red-500/10';
default:
return 'border-gray-500/30 bg-gray-500/10';
}
};

const formatDuration = (ms: number) => {
if (ms < 1000) return `${ms}ms`;
return `${(ms / 1000).toFixed(2)}s`;
};

const renderStep = (step: ExecutionStep, depth: number = 0) => {
const isStepExpanded = expandedSteps.has(step.id);
const hasSubsteps = step.substeps && step.substeps.length > 0;

return (
<div key={step.id} className={`${depth > 0 ? 'ml-6' : ''}`}>
<div
className={`border rounded-lg p-3 mb-2 cursor-pointer transition-all hover:shadow-md ${getStepColor(step.type)}`}
onClick={() => hasSubsteps && toggleStep(step.id)}
>
<div className="flex items-start gap-3">
<div className="flex items-center gap-2 flex-1">
{hasSubsteps && (
<span className="text-gray-400">
{isStepExpanded ? <ChevronDown className="h-4 w-4" /> : <ChevronRight className="h-4 w-4" />}
</span>
)}
{getStepIcon(step.type)}
<div className="flex-1">
<div className="flex items-center justify-between">
<h4 className="font-semibold text-white text-sm">{step.name}</h4>
<div className="flex items-center gap-2 text-xs text-gray-400">
<Clock className="h-3 w-3" />
<span>{formatDuration(step.duration)}</span>
</div>
</div>
<p className="text-gray-300 text-sm mt-1">{step.description}</p>
</div>
</div>
</div>

{/* Step Details */}
{step.details && (
<div className="mt-3 p-2 bg-black/20 rounded text-xs">
<pre className="text-gray-300 whitespace-pre-wrap">
{typeof step.details === 'string'
? step.details
: JSON.stringify(step.details, null, 2)
}
</pre>
</div>
)}
</div>

{/* Substeps */}
{hasSubsteps && isStepExpanded && (
<div className="mt-2">
{step.substeps!.map(substep => renderStep(substep, depth + 1))}
</div>
)}
</div>
);
};

if (!trace || !trace.steps || trace.steps.length === 0) {
return null;
}

return (
<div className="mt-4 border border-gray-700 rounded-lg overflow-hidden">
{/* Header */}
<div
className="bg-gray-800/50 px-4 py-3 cursor-pointer hover:bg-gray-800/70 transition-colors"
onClick={() => setIsExpanded(!isExpanded)}
>
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<Cpu className="h-4 w-4 text-blue-400" />
<span className="text-sm font-medium text-white">Execution Trace</span>
<span className="text-xs text-gray-400">({trace.steps.length} steps)</span>
</div>
<div className="flex items-center gap-3">
<div className="flex items-center gap-1 text-xs text-gray-400">
<Clock className="h-3 w-3" />
<span>{formatDuration(trace.totalTime)}</span>
</div>
<ChevronDown
className={`h-4 w-4 text-gray-400 transition-transform ${isExpanded ? 'rotate-180' : ''}`}
/>
</div>
</div>
</div>

{/* Content */}
{isExpanded && (
<div className="p-4 bg-gray-900/30 max-h-96 overflow-y-auto">
{/* Summary */}
<div className="mb-4 p-3 bg-gray-800/50 rounded-lg">
<div className="grid grid-cols-2 gap-4 text-sm">
<div>
<span className="text-gray-400">Start Time:</span>
<span className="text-white ml-2">
{new Date(trace.startTime).toLocaleTimeString()}
</span>
</div>
<div>
<span className="text-gray-400">End Time:</span>
<span className="text-white ml-2">
{new Date(trace.endTime).toLocaleTimeString()}
</span>
</div>
<div>
<span className="text-gray-400">Total Duration:</span>
<span className="text-white ml-2">{formatDuration(trace.totalTime)}</span>
</div>
<div>
<span className="text-gray-400">Steps:</span>
<span className="text-white ml-2">{trace.steps.length}</span>
</div>
</div>
</div>

{/* Steps */}
<div className="space-y-2">
{trace.steps.map(step => renderStep(step))}
</div>
</div>
)}
</div>
);
}
130 changes: 130 additions & 0 deletions src/components/examples/ExecutionTraceDemo.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
'use client';

import React from 'react';
import { ExecutionTrace } from '@/types';
import ExecutionTraceComponent from '@/components/chat/ExecutionTrace';

// Sample execution trace data for demonstration
const sampleExecutionTrace: ExecutionTrace = {
steps: [
{
id: '1',
name: 'Understanding User Query',
type: 'thought',
timestamp: '2024-03-27T13:15:00.000Z',
duration: 150,
description: 'Analyzing the user query about vault strategies and identifying key requirements',
details: {
query: 'What are the best vault strategies for high yield?',
intent: 'User wants investment recommendations',
entities: ['vault strategies', 'high yield', 'investment']
}
},
{
id: '2',
name: 'Fetching Vault Data',
type: 'action',
timestamp: '2024-03-27T13:15:00.150Z',
duration: 300,
description: 'Retrieving current vault information and performance data',
substeps: [
{
id: '2.1',
name: 'Query Vault Registry',
type: 'tool_call',
timestamp: '2024-03-27T13:15:00.200Z',
duration: 120,
description: 'Calling vault registry API to get available vaults',
details: {
tool: 'vault_registry',
parameters: { include_performance: true },
result: { vaults_found: 15 }
}
},
{
id: '2.2',
name: 'Filter High-Performance Vaults',
type: 'action',
timestamp: '2024-03-27T13:15:00.320Z',
duration: 80,
description: 'Filtering vaults based on APY and risk metrics',
details: {
criteria: { min_apy: 8, max_risk: 'medium' },
filtered_count: 7
}
}
]
},
{
id: '3',
name: 'Analysis and Ranking',
type: 'thought',
timestamp: '2024-03-27T13:15:00.450Z',
duration: 200,
description: 'Analyzing vault performance metrics and ranking by risk-adjusted returns',
details: {
metrics: ['apy', 'tvl', 'volatility', 'risk_score'],
ranking_method: 'sharpe_ratio_weighted'
}
},
{
id: '4',
name: 'Generate Recommendations',
type: 'result',
timestamp: '2024-03-27T13:15:01.150Z',
duration: 100,
description: 'Creating personalized recommendations based on user preferences',
details: {
recommendations: [
{
vault_name: 'Stable Yield Plus',
apy: 12.5,
risk_level: 'low',
confidence: 0.95
},
{
vault_name: 'Growth Strategy Alpha',
apy: 18.2,
risk_level: 'medium',
confidence: 0.88
}
]
}
}
],
totalTime: 750,
startTime: '2024-03-27T13:15:00.000Z',
endTime: '2024-03-27T13:15:01.150Z'
};

export function ExecutionTraceDemo() {
return (
<div className="p-6 max-w-4xl mx-auto">
<div className="mb-6">
<h2 className="text-2xl font-bold text-white mb-2">Execution Trace Demo</h2>
<p className="text-gray-400">
This demonstrates how execution traces will appear when the backend provides agent thought-processes.
</p>
</div>

<div className="bg-gray-800 rounded-lg p-4 mb-6">
<h3 className="text-lg font-semibold text-white mb-2">Sample Agent Response:</h3>
<div className="text-gray-300">
Based on your query about high-yield vault strategies, I've analyzed the current market and identified two excellent options:

<div className="mt-4 p-3 bg-gray-700 rounded">
<strong>🏦 Stable Yield Plus</strong> - 12.5% APY, Low Risk
<br />
<strong>📈 Growth Strategy Alpha</strong> - 18.2% APY, Medium Risk
</div>
</div>
</div>

<ExecutionTraceComponent trace={sampleExecutionTrace} />

<div className="mt-6 text-sm text-gray-400">
<p><strong>Note:</strong> This is sample data. In production, execution traces will be automatically populated from the backend API response.</p>
</div>
</div>
);
}
34 changes: 8 additions & 26 deletions src/store/slices/chatSlice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,41 +103,23 @@ export const sendMessage = createAsyncThunk(
timestamp: new Date().toISOString(),
};

// Mock agent response
const mockResponse = {
result: {
success: true,
data: `Mock agent response to: "${query}". This is a simulated response since backend is disconnected.`,
error: null,
metadata: {
type: 'info',
action: 'mock',
amount: null,
asset: null,
requiresConfirmation: false,
}
}
};
// Call the API service to get actual response
const response = await apiService.queryAgent({ userId, query });

// Save agent response locally (no server call needed)
// Create agent message with execution trace if available
const agentMessage: ChatMessage = {
id: `msg_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
type: 'agent',
content: mockResponse.result.data,
content: response.result.data,
timestamp: new Date().toISOString(),
metadata: {
success: mockResponse.result.success,
error: mockResponse.result.error,
transactionHash: null,
type: mockResponse.result.metadata?.type,
action: mockResponse.result.metadata?.action,
amount: mockResponse.result.metadata?.amount,
asset: mockResponse.result.metadata?.asset,
requiresConfirmation: mockResponse.result.metadata?.requiresConfirmation,
success: response.result.success,
error: response.result.error,
executionTrace: response.result.executionTrace,
}
};

return { response: mockResponse, conversation, userMessage, agentMessage };
return { response, conversation, userMessage, agentMessage };
} catch (error: any) {
return rejectWithValue(error.response?.data?.message || 'Failed to send message');
}
Expand Down
Loading
Loading