Fix duplicate key warnings and improve inference message details
- Fix duplicate React keys by using unique IDs for request/response/error messages - Show complete JSON data in Request/Response/Error details sections - Add Message Metadata section showing all message properties - Remove debug console.log statements 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
91608a0b97
commit
4771d8f1c8
8 changed files with 496 additions and 127 deletions
186
src/components/InferenceMessageMonitor.tsx
Normal file
186
src/components/InferenceMessageMonitor.tsx
Normal file
|
|
@ -0,0 +1,186 @@
|
||||||
|
// Component to monitor and display inference messages
|
||||||
|
|
||||||
|
import { useState, useRef } from 'react';
|
||||||
|
import { useInference } from '@/contexts/InferenceContext';
|
||||||
|
import type { InferenceMessage } from '@/types/inference';
|
||||||
|
|
||||||
|
export function InferenceMessageMonitor() {
|
||||||
|
const { messages, clearMessages } = useInference();
|
||||||
|
const [maxMessages, setMaxMessages] = useState(50);
|
||||||
|
const messagesEndRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
// Slice messages to show only the latest maxMessages
|
||||||
|
const displayMessages = messages.slice(-maxMessages);
|
||||||
|
|
||||||
|
const getMessageTypeColor = (type: InferenceMessage['type']) => {
|
||||||
|
switch (type) {
|
||||||
|
case 'request':
|
||||||
|
return 'bg-blue-50 dark:bg-blue-900/30 border-blue-200 dark:border-blue-800';
|
||||||
|
case 'response':
|
||||||
|
return 'bg-green-50 dark:bg-green-900/30 border-green-200 dark:border-green-800';
|
||||||
|
case 'error':
|
||||||
|
return 'bg-red-50 dark:bg-red-900/30 border-red-200 dark:border-red-800';
|
||||||
|
case 'stream_chunk':
|
||||||
|
return 'bg-yellow-50 dark:bg-yellow-900/30 border-yellow-200 dark:border-yellow-800';
|
||||||
|
default:
|
||||||
|
return 'bg-gray-50 dark:bg-gray-900/30 border-gray-200 dark:border-gray-800';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatDuration = (duration?: number) => {
|
||||||
|
if (!duration) return '';
|
||||||
|
if (duration < 1000) return `${duration}ms`;
|
||||||
|
return `${(duration / 1000).toFixed(2)}s`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatMessage = (message: InferenceMessage) => {
|
||||||
|
switch (message.type) {
|
||||||
|
case 'request':
|
||||||
|
return `Request to ${message.model || 'default model'}`;
|
||||||
|
case 'response':
|
||||||
|
return `Response from ${message.model || 'default model'}`;
|
||||||
|
case 'error':
|
||||||
|
return `Error from ${message.model || 'default model'}`;
|
||||||
|
case 'stream_chunk':
|
||||||
|
return `Stream chunk from ${message.model || 'default model'}`;
|
||||||
|
default:
|
||||||
|
return 'Unknown message type';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex-1 flex flex-col border-t border-gray-200 dark:border-gray-700 min-h-0">
|
||||||
|
<div className="flex items-center justify-between p-4 pb-2 flex-shrink-0">
|
||||||
|
<h3 className="font-medium text-gray-900 dark:text-white">
|
||||||
|
Inference Monitor ({displayMessages.length})
|
||||||
|
</h3>
|
||||||
|
<button
|
||||||
|
onClick={clearMessages}
|
||||||
|
className="text-xs px-2 py-1 bg-red-100 dark:bg-red-900/50 text-red-800 dark:text-red-200 rounded hover:bg-red-200 dark:hover:bg-red-800"
|
||||||
|
>
|
||||||
|
Clear
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex-1 mx-4 mb-2 overflow-y-auto border border-gray-200 dark:border-gray-600 rounded-lg min-h-0">
|
||||||
|
{displayMessages.length === 0 ? (
|
||||||
|
<div className="p-4 text-center text-gray-500 dark:text-gray-400 text-sm">
|
||||||
|
No messages yet. Messages will appear here as inference requests are made.
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="p-2 space-y-2">
|
||||||
|
{displayMessages.map((message) => (
|
||||||
|
<div
|
||||||
|
key={message.id}
|
||||||
|
className={`p-2 rounded border text-xs ${getMessageTypeColor(message.type)}`}
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-between mb-1">
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<span className={
|
||||||
|
message.type === 'request' ? 'text-blue-600 dark:text-blue-400' :
|
||||||
|
message.type === 'response' ? 'text-green-600 dark:text-green-400' :
|
||||||
|
message.type === 'error' ? 'text-red-600 dark:text-red-400' :
|
||||||
|
'text-yellow-600 dark:text-yellow-400'
|
||||||
|
}>
|
||||||
|
{message.type === 'request' ? '→' :
|
||||||
|
message.type === 'response' ? '←' :
|
||||||
|
message.type === 'error' ? '✗' : '⋯'}
|
||||||
|
</span>
|
||||||
|
<span className="font-medium text-gray-900 dark:text-white">
|
||||||
|
{formatMessage(message)}
|
||||||
|
</span>
|
||||||
|
{message.duration && (
|
||||||
|
<span className="text-gray-500 dark:text-gray-400">
|
||||||
|
({formatDuration(message.duration)})
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<span className="text-gray-500 dark:text-gray-400">
|
||||||
|
{message.timestamp.toLocaleTimeString()}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Message metadata */}
|
||||||
|
<details className="text-gray-600 dark:text-gray-300 mt-2">
|
||||||
|
<summary className="cursor-pointer hover:text-gray-800 dark:hover:text-gray-100">
|
||||||
|
Message Metadata
|
||||||
|
</summary>
|
||||||
|
<pre className="mt-1 text-xs bg-gray-100 dark:bg-gray-800 p-2 rounded overflow-x-auto">
|
||||||
|
{JSON.stringify({
|
||||||
|
id: message.id,
|
||||||
|
type: message.type,
|
||||||
|
providerId: message.providerId,
|
||||||
|
providerName: message.providerName,
|
||||||
|
model: message.model,
|
||||||
|
timestamp: message.timestamp.toISOString(),
|
||||||
|
duration: message.duration
|
||||||
|
}, null, 2)}
|
||||||
|
</pre>
|
||||||
|
</details>
|
||||||
|
|
||||||
|
{/* Request details */}
|
||||||
|
{message.request && (
|
||||||
|
<details className="text-gray-600 dark:text-gray-300 mt-2">
|
||||||
|
<summary className="cursor-pointer hover:text-gray-800 dark:hover:text-gray-100">
|
||||||
|
Request Details
|
||||||
|
</summary>
|
||||||
|
<pre className="mt-1 text-xs bg-gray-100 dark:bg-gray-800 p-2 rounded overflow-x-auto">
|
||||||
|
{JSON.stringify(message.request, null, 2)}
|
||||||
|
</pre>
|
||||||
|
</details>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Response details */}
|
||||||
|
{message.response && (
|
||||||
|
<details className="text-gray-600 dark:text-gray-300 mt-2">
|
||||||
|
<summary className="cursor-pointer hover:text-gray-800 dark:hover:text-gray-100">
|
||||||
|
Response Details
|
||||||
|
</summary>
|
||||||
|
<pre className="mt-1 text-xs bg-gray-100 dark:bg-gray-800 p-2 rounded overflow-x-auto">
|
||||||
|
{JSON.stringify(message.response, null, 2)}
|
||||||
|
</pre>
|
||||||
|
</details>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Error details */}
|
||||||
|
{message.error && (
|
||||||
|
<details className="text-gray-600 dark:text-gray-300 mt-2">
|
||||||
|
<summary className="cursor-pointer hover:text-gray-800 dark:hover:text-gray-100">
|
||||||
|
Error Details
|
||||||
|
</summary>
|
||||||
|
<pre className="mt-1 text-xs bg-gray-100 dark:bg-gray-800 p-2 rounded overflow-x-auto">
|
||||||
|
{message.error instanceof Error ? message.error.message : JSON.stringify(message.error, null, 2)}
|
||||||
|
</pre>
|
||||||
|
</details>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
<div ref={messagesEndRef} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Controls */}
|
||||||
|
<div className="p-4 pt-2 flex items-center justify-between text-xs border-t border-gray-200 dark:border-gray-700 flex-shrink-0">
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<label className="text-gray-600 dark:text-gray-400">
|
||||||
|
Max messages:
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
value={maxMessages}
|
||||||
|
onChange={(e) => setMaxMessages(Number(e.target.value))}
|
||||||
|
className="px-2 py-1 border border-gray-300 dark:border-gray-600 rounded bg-white dark:bg-gray-700 text-gray-900 dark:text-white"
|
||||||
|
>
|
||||||
|
<option value={25}>25</option>
|
||||||
|
<option value={50}>50</option>
|
||||||
|
<option value={100}>100</option>
|
||||||
|
<option value={200}>200</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div className="text-gray-500 dark:text-gray-400">
|
||||||
|
Live monitoring active
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -3,10 +3,13 @@
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { useInference } from '@/contexts/InferenceContext';
|
import { useInference } from '@/contexts/InferenceContext';
|
||||||
import { OpenRouterOAuthProvider } from '@/providers/openrouter/oauth-provider';
|
import { OpenRouterOAuthProvider } from '@/providers/openrouter/oauth-provider';
|
||||||
|
import { InferenceMessageMonitor } from './InferenceMessageMonitor';
|
||||||
|
|
||||||
export function InferenceTab() {
|
export function InferenceTab() {
|
||||||
const { provider, models, clearProvider, setProvider } = useInference();
|
const { provider, models, clearProvider, setProvider } = useInference();
|
||||||
const [isConnecting, setIsConnecting] = useState(false);
|
const [isConnecting, setIsConnecting] = useState(false);
|
||||||
|
const [showConnectionStatus, setShowConnectionStatus] = useState(true);
|
||||||
|
const [showModels, setShowModels] = useState(false);
|
||||||
|
|
||||||
const handleConnect = async () => {
|
const handleConnect = async () => {
|
||||||
setIsConnecting(true);
|
setIsConnecting(true);
|
||||||
|
|
@ -28,88 +31,112 @@ export function InferenceTab() {
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex-1 overflow-y-auto p-4">
|
<div className="h-full flex flex-col overflow-hidden">
|
||||||
<div className="space-y-4">
|
{/* Connection Status */}
|
||||||
{/* Connection Status */}
|
<div className="border-b border-gray-200 dark:border-gray-700">
|
||||||
<div className="bg-gray-50 dark:bg-gray-700 rounded-lg p-3">
|
<button
|
||||||
<h3 className="font-medium text-gray-900 dark:text-white mb-2">
|
onClick={() => setShowConnectionStatus(!showConnectionStatus)}
|
||||||
Inference Provider
|
className="w-full p-4 text-left hover:bg-gray-50 dark:hover:bg-gray-700/50 transition-colors"
|
||||||
</h3>
|
>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
{provider ? (
|
<h3 className="font-medium text-gray-900 dark:text-white">
|
||||||
<div className="space-y-2">
|
Connection Status
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<div>
|
|
||||||
<p className="text-sm text-gray-700 dark:text-gray-300">
|
|
||||||
<span className="text-green-600 dark:text-green-400">🟢</span> Connected to OpenRouter
|
|
||||||
</p>
|
|
||||||
<p className="text-xs text-gray-500 dark:text-gray-400">
|
|
||||||
{models.length} models available
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
onClick={handleDisconnect}
|
|
||||||
className="px-3 py-1 text-xs bg-red-100 dark:bg-red-900/50 text-red-800 dark:text-red-200 rounded hover:bg-red-200 dark:hover:bg-red-800"
|
|
||||||
>
|
|
||||||
Disconnect
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="space-y-2">
|
|
||||||
<p className="text-sm text-gray-700 dark:text-gray-300">
|
|
||||||
<span className="text-gray-400">⚪</span> Not connected
|
|
||||||
</p>
|
|
||||||
<button
|
|
||||||
onClick={handleConnect}
|
|
||||||
disabled={isConnecting}
|
|
||||||
className="w-full px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:bg-gray-400 disabled:cursor-not-allowed transition-colors"
|
|
||||||
>
|
|
||||||
{isConnecting ? 'Connecting...' : 'Connect to OpenRouter'}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Available Models */}
|
|
||||||
{provider && models.length > 0 && (
|
|
||||||
<div>
|
|
||||||
<h3 className="font-medium text-gray-900 dark:text-white mb-3">
|
|
||||||
Available Models ({models.length})
|
|
||||||
</h3>
|
</h3>
|
||||||
<div className="space-y-1 max-h-64 overflow-y-auto">
|
<span className="text-gray-500 dark:text-gray-400">
|
||||||
{models.map((model) => (
|
{showConnectionStatus ? '−' : '+'}
|
||||||
<div
|
</span>
|
||||||
key={model.id}
|
</div>
|
||||||
className="text-xs bg-gray-50 dark:bg-gray-700 rounded px-2 py-2"
|
</button>
|
||||||
>
|
{showConnectionStatus && (
|
||||||
<div className="font-mono text-gray-900 dark:text-white">
|
<div className="px-4 pb-4">
|
||||||
{model.id}
|
<div className="bg-gray-50 dark:bg-gray-700 rounded-lg p-3">
|
||||||
</div>
|
{provider ? (
|
||||||
{model.name && model.name !== model.id && (
|
<div className="space-y-2">
|
||||||
<div className="text-gray-600 dark:text-gray-300 mt-1">
|
<div className="flex items-center justify-between">
|
||||||
{model.name}
|
<div>
|
||||||
|
<p className="text-sm text-gray-700 dark:text-gray-300">
|
||||||
|
<span className="text-green-600 dark:text-green-400">🟢</span> Connected to {provider.name}
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-gray-500 dark:text-gray-400">
|
||||||
|
{models.length} models available
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
<button
|
||||||
<div className="text-gray-500 dark:text-gray-400 mt-1">
|
onClick={handleDisconnect}
|
||||||
Context: {model.contextLength?.toLocaleString() || 'Unknown'}
|
className="px-3 py-1 text-xs bg-red-100 dark:bg-red-900/50 text-red-800 dark:text-red-200 rounded hover:bg-red-200 dark:hover:bg-red-800"
|
||||||
|
>
|
||||||
|
Disconnect
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
) : (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<p className="text-sm text-gray-700 dark:text-gray-300">
|
||||||
|
<span className="text-gray-400">⚪</span> Not connected
|
||||||
|
</p>
|
||||||
|
<button
|
||||||
|
onClick={handleConnect}
|
||||||
|
disabled={isConnecting}
|
||||||
|
className="w-full px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:bg-gray-400 disabled:cursor-not-allowed transition-colors"
|
||||||
|
>
|
||||||
|
{isConnecting ? 'Connecting...' : 'Connect to OpenRouter'}
|
||||||
|
</button>
|
||||||
|
<div className="text-xs text-gray-500 dark:text-gray-400 mt-2 space-y-1">
|
||||||
|
<p>Connect to OpenRouter to access AI models for conversations.</p>
|
||||||
|
<p>You'll need an OpenRouter account and API key to authenticate.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Help Text */}
|
|
||||||
<div className="text-xs text-gray-500 dark:text-gray-400 space-y-1">
|
|
||||||
<p>
|
|
||||||
Connect to OpenRouter to access AI models for conversations.
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
You'll need an OpenRouter account and API key to authenticate.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Available Models */}
|
||||||
|
{provider && models.length > 0 && (
|
||||||
|
<div className="flex-shrink-0 border-b border-gray-200 dark:border-gray-700">
|
||||||
|
<button
|
||||||
|
onClick={() => setShowModels(!showModels)}
|
||||||
|
className="w-full p-4 text-left hover:bg-gray-50 dark:hover:bg-gray-700/50 transition-colors"
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<h3 className="font-medium text-gray-900 dark:text-white">
|
||||||
|
Available Models ({models.length})
|
||||||
|
</h3>
|
||||||
|
<span className="text-gray-500 dark:text-gray-400">
|
||||||
|
{showModels ? '−' : '+'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
{showModels && (
|
||||||
|
<div className="px-4 pb-4">
|
||||||
|
<div className="space-y-1 max-h-60 overflow-y-auto">
|
||||||
|
{models.map((model) => (
|
||||||
|
<div
|
||||||
|
key={model.id}
|
||||||
|
className="text-xs bg-gray-50 dark:bg-gray-700 rounded px-2 py-2"
|
||||||
|
>
|
||||||
|
<div className="font-mono text-gray-900 dark:text-white">
|
||||||
|
{model.id}
|
||||||
|
</div>
|
||||||
|
{model.name && model.name !== model.id && (
|
||||||
|
<div className="text-gray-600 dark:text-gray-300 mt-1">
|
||||||
|
{model.name}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="text-gray-500 dark:text-gray-400 mt-1">
|
||||||
|
Context: {model.contextLength?.toLocaleString() || 'Unknown'}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Inference Message Monitor */}
|
||||||
|
<InferenceMessageMonitor />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -1,53 +1,16 @@
|
||||||
// Component to monitor and display MCP messages
|
// Component to monitor and display MCP messages
|
||||||
|
|
||||||
import { useState, useEffect, useRef } from 'react';
|
import { useState, useRef } from 'react';
|
||||||
import { useMCP } from '@/contexts/MCPContext';
|
import { useMCP } from '@/contexts/MCPContext';
|
||||||
import type { JSONRPCMessage } from '@modelcontextprotocol/sdk/types.js';
|
import type { JSONRPCMessage } from '@modelcontextprotocol/sdk/types.js';
|
||||||
|
|
||||||
interface MCPMessageEntry {
|
|
||||||
id: string;
|
|
||||||
timestamp: Date;
|
|
||||||
connectionId: string;
|
|
||||||
connectionName: string;
|
|
||||||
direction: 'sent' | 'received';
|
|
||||||
message: JSONRPCMessage;
|
|
||||||
extra?: any;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function MCPMessageMonitor() {
|
export function MCPMessageMonitor() {
|
||||||
const { addMessageCallback, removeMessageCallback, connections } = useMCP();
|
const { messages, clearMessages } = useMCP();
|
||||||
const [messages, setMessages] = useState<MCPMessageEntry[]>([]);
|
|
||||||
const [maxMessages, setMaxMessages] = useState(50);
|
const [maxMessages, setMaxMessages] = useState(50);
|
||||||
const messagesEndRef = useRef<HTMLDivElement>(null);
|
const messagesEndRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
// Remove auto-scroll - let user control scroll position
|
// Slice messages to show only the latest maxMessages
|
||||||
|
const displayMessages = messages.slice(-maxMessages);
|
||||||
// Register message callback
|
|
||||||
useEffect(() => {
|
|
||||||
const callbackId = addMessageCallback((connectionId, _client, message, direction, extra) => {
|
|
||||||
const connection = connections.find(c => c.id === connectionId);
|
|
||||||
|
|
||||||
setMessages(prev => {
|
|
||||||
const newMessage: MCPMessageEntry = {
|
|
||||||
id: `${Date.now()}-${Math.random()}`,
|
|
||||||
timestamp: new Date(),
|
|
||||||
connectionId,
|
|
||||||
connectionName: connection?.name || 'Unknown',
|
|
||||||
direction,
|
|
||||||
message,
|
|
||||||
extra,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Keep only the latest maxMessages
|
|
||||||
const newMessages = [...prev, newMessage];
|
|
||||||
return newMessages.slice(-maxMessages);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
removeMessageCallback(callbackId);
|
|
||||||
};
|
|
||||||
}, [addMessageCallback, removeMessageCallback, connections, maxMessages]);
|
|
||||||
|
|
||||||
const formatMessage = (msg: JSONRPCMessage) => {
|
const formatMessage = (msg: JSONRPCMessage) => {
|
||||||
if ('method' in msg) {
|
if ('method' in msg) {
|
||||||
|
|
@ -74,15 +37,12 @@ export function MCPMessageMonitor() {
|
||||||
return baseClass;
|
return baseClass;
|
||||||
};
|
};
|
||||||
|
|
||||||
const clearMessages = () => {
|
|
||||||
setMessages([]);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex-1 flex flex-col border-t border-gray-200 dark:border-gray-700 min-h-0">
|
<div className="flex-1 flex flex-col border-t border-gray-200 dark:border-gray-700 min-h-0">
|
||||||
<div className="flex items-center justify-between p-4 pb-2 flex-shrink-0">
|
<div className="flex items-center justify-between p-4 pb-2 flex-shrink-0">
|
||||||
<h3 className="font-medium text-gray-900 dark:text-white">
|
<h3 className="font-medium text-gray-900 dark:text-white">
|
||||||
Message Monitor ({messages.length})
|
Message Monitor ({displayMessages.length})
|
||||||
</h3>
|
</h3>
|
||||||
<button
|
<button
|
||||||
onClick={clearMessages}
|
onClick={clearMessages}
|
||||||
|
|
@ -93,13 +53,13 @@ export function MCPMessageMonitor() {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex-1 mx-4 mb-2 overflow-y-auto border border-gray-200 dark:border-gray-600 rounded-lg">
|
<div className="flex-1 mx-4 mb-2 overflow-y-auto border border-gray-200 dark:border-gray-600 rounded-lg">
|
||||||
{messages.length === 0 ? (
|
{displayMessages.length === 0 ? (
|
||||||
<div className="p-4 text-center text-gray-500 dark:text-gray-400 text-sm">
|
<div className="p-4 text-center text-gray-500 dark:text-gray-400 text-sm">
|
||||||
No messages yet. Messages will appear here as they're sent/received.
|
No messages yet. Messages will appear here as they're sent/received.
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="p-2 space-y-2">
|
<div className="p-2 space-y-2">
|
||||||
{messages.map((entry) => (
|
{displayMessages.map((entry) => (
|
||||||
<div
|
<div
|
||||||
key={entry.id}
|
key={entry.id}
|
||||||
className={`p-2 rounded border text-xs ${getMessageTypeColor(entry.message, entry.direction)}`}
|
className={`p-2 rounded border text-xs ${getMessageTypeColor(entry.message, entry.direction)}`}
|
||||||
|
|
@ -113,7 +73,7 @@ export function MCPMessageMonitor() {
|
||||||
{formatMessage(entry.message)}
|
{formatMessage(entry.message)}
|
||||||
</span>
|
</span>
|
||||||
<span className="text-gray-500 dark:text-gray-400">
|
<span className="text-gray-500 dark:text-gray-400">
|
||||||
{entry.connectionName}
|
{entry.serverName}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<span className="text-gray-500 dark:text-gray-400">
|
<span className="text-gray-500 dark:text-gray-400">
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,10 @@ import type {
|
||||||
InferenceRequest,
|
InferenceRequest,
|
||||||
InferenceResponse,
|
InferenceResponse,
|
||||||
Model,
|
Model,
|
||||||
|
InferenceMessage,
|
||||||
|
InferenceMessageCallback,
|
||||||
} from '@/types/inference';
|
} from '@/types/inference';
|
||||||
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
import { OpenRouterApiProvider } from '@/providers/openrouter/api-provider';
|
import { OpenRouterApiProvider } from '@/providers/openrouter/api-provider';
|
||||||
import { OpenRouterOAuthProvider } from '@/providers/openrouter/oauth-provider';
|
import { OpenRouterOAuthProvider } from '@/providers/openrouter/oauth-provider';
|
||||||
|
|
||||||
|
|
@ -30,6 +33,12 @@ interface InferenceContextValue {
|
||||||
models: Model[];
|
models: Model[];
|
||||||
selectedModel: Model | undefined;
|
selectedModel: Model | undefined;
|
||||||
isAuthenticated: boolean;
|
isAuthenticated: boolean;
|
||||||
|
|
||||||
|
// Message monitoring
|
||||||
|
messages: InferenceMessage[];
|
||||||
|
addMessageCallback: (callback: InferenceMessageCallback) => string; // Returns callback ID
|
||||||
|
removeMessageCallback: (callbackId: string) => void;
|
||||||
|
clearMessages: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const InferenceContext = createContext<InferenceContextValue | null>(null);
|
const InferenceContext = createContext<InferenceContextValue | null>(null);
|
||||||
|
|
@ -47,6 +56,22 @@ export function InferenceContextProvider({ children }: InferenceProviderProps) {
|
||||||
return localStorage.getItem('selected_model_id') || undefined;
|
return localStorage.getItem('selected_model_id') || undefined;
|
||||||
});
|
});
|
||||||
const [_, setAuthStateVersion] = useState(0); // Force re-renders on auth changes
|
const [_, setAuthStateVersion] = useState(0); // Force re-renders on auth changes
|
||||||
|
|
||||||
|
// Message callback management
|
||||||
|
const [messageCallbacks, setMessageCallbacks] = useState<Map<string, InferenceMessageCallback>>(new Map());
|
||||||
|
|
||||||
|
// Store messages in context for persistence
|
||||||
|
const [messages, setMessages] = useState<InferenceMessage[]>([]);
|
||||||
|
const maxMessages = 100; // Keep last 100 messages
|
||||||
|
|
||||||
|
// Function to add a message to the stored messages
|
||||||
|
const addMessage = useCallback((message: InferenceMessage) => {
|
||||||
|
setMessages(prev => {
|
||||||
|
const newMessages = [...prev, message];
|
||||||
|
// Keep only the latest maxMessages
|
||||||
|
return newMessages.slice(-maxMessages);
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
const setProvider = useCallback((newProvider: InferenceProvider) => {
|
const setProvider = useCallback((newProvider: InferenceProvider) => {
|
||||||
setProviderState(newProvider);
|
setProviderState(newProvider);
|
||||||
|
|
@ -136,18 +161,91 @@ export function InferenceContextProvider({ children }: InferenceProviderProps) {
|
||||||
|
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
|
|
||||||
|
const requestId = uuidv4();
|
||||||
|
const startTime = Date.now();
|
||||||
|
|
||||||
|
// Broadcast request message
|
||||||
|
const requestMessage: InferenceMessage = {
|
||||||
|
id: `${requestId}-request`,
|
||||||
|
timestamp: new Date(),
|
||||||
|
type: 'request',
|
||||||
|
providerId: provider.id,
|
||||||
|
providerName: provider.name,
|
||||||
|
model: request.model,
|
||||||
|
request,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Store message in context
|
||||||
|
addMessage(requestMessage);
|
||||||
|
|
||||||
|
messageCallbacks.forEach(callback => {
|
||||||
|
try {
|
||||||
|
callback(requestMessage);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error in inference message callback:', error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await provider.generateResponse(request);
|
const response = await provider.generateResponse(request);
|
||||||
|
|
||||||
|
// Broadcast response message
|
||||||
|
const responseMessage: InferenceMessage = {
|
||||||
|
id: `${requestId}-response`,
|
||||||
|
timestamp: new Date(),
|
||||||
|
type: 'response',
|
||||||
|
providerId: provider.id,
|
||||||
|
providerName: provider.name,
|
||||||
|
model: request.model,
|
||||||
|
response,
|
||||||
|
duration: Date.now() - startTime,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Store message in context
|
||||||
|
addMessage(responseMessage);
|
||||||
|
|
||||||
|
messageCallbacks.forEach(callback => {
|
||||||
|
try {
|
||||||
|
callback(responseMessage);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error in inference message callback:', error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
return response;
|
return response;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const errorMessage = err instanceof Error ? err.message : 'Inference request failed';
|
const errorMessage = err instanceof Error ? err.message : 'Inference request failed';
|
||||||
setError(errorMessage);
|
setError(errorMessage);
|
||||||
|
|
||||||
|
// Broadcast error message
|
||||||
|
const errorMessageObj: InferenceMessage = {
|
||||||
|
id: `${requestId}-error`,
|
||||||
|
timestamp: new Date(),
|
||||||
|
type: 'error',
|
||||||
|
providerId: provider.id,
|
||||||
|
providerName: provider.name,
|
||||||
|
model: request.model,
|
||||||
|
error: err,
|
||||||
|
duration: Date.now() - startTime,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Store message in context
|
||||||
|
addMessage(errorMessageObj);
|
||||||
|
|
||||||
|
messageCallbacks.forEach(callback => {
|
||||||
|
try {
|
||||||
|
callback(errorMessageObj);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error in inference message callback:', error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
throw err;
|
throw err;
|
||||||
} finally {
|
} finally {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
}
|
}
|
||||||
}, [provider]);
|
}, [provider, messageCallbacks, addMessage]);
|
||||||
|
|
||||||
const selectModel = useCallback((modelId: string) => {
|
const selectModel = useCallback((modelId: string) => {
|
||||||
if (!provider) {
|
if (!provider) {
|
||||||
|
|
@ -209,6 +307,25 @@ export function InferenceContextProvider({ children }: InferenceProviderProps) {
|
||||||
}
|
}
|
||||||
}, [provider]);
|
}, [provider]);
|
||||||
|
|
||||||
|
// Message callback management
|
||||||
|
const addMessageCallback = useCallback((callback: InferenceMessageCallback): string => {
|
||||||
|
const callbackId = uuidv4();
|
||||||
|
setMessageCallbacks(prev => new Map(prev).set(callbackId, callback));
|
||||||
|
return callbackId;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const removeMessageCallback = useCallback((callbackId: string) => {
|
||||||
|
setMessageCallbacks(prev => {
|
||||||
|
const newMap = new Map(prev);
|
||||||
|
newMap.delete(callbackId);
|
||||||
|
return newMap;
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const clearMessages = useCallback(() => {
|
||||||
|
setMessages([]);
|
||||||
|
}, []);
|
||||||
|
|
||||||
const contextValue: InferenceContextValue = {
|
const contextValue: InferenceContextValue = {
|
||||||
provider,
|
provider,
|
||||||
isLoading,
|
isLoading,
|
||||||
|
|
@ -222,6 +339,10 @@ export function InferenceContextProvider({ children }: InferenceProviderProps) {
|
||||||
models: provider?.models || [],
|
models: provider?.models || [],
|
||||||
selectedModel: selectedModelId ? provider?.models.find(m => m.id === selectedModelId) : undefined,
|
selectedModel: selectedModelId ? provider?.models.find(m => m.id === selectedModelId) : undefined,
|
||||||
isAuthenticated: provider?.isAuthenticated || false,
|
isAuthenticated: provider?.isAuthenticated || false,
|
||||||
|
messages,
|
||||||
|
addMessageCallback,
|
||||||
|
removeMessageCallback,
|
||||||
|
clearMessages,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ import type {
|
||||||
MCPResource,
|
MCPResource,
|
||||||
MCPContextValue,
|
MCPContextValue,
|
||||||
MCPMessageCallback,
|
MCPMessageCallback,
|
||||||
|
MCPMonitorMessage,
|
||||||
} from '@/types/mcp';
|
} from '@/types/mcp';
|
||||||
import type { Tool } from '@/types/inference';
|
import type { Tool } from '@/types/inference';
|
||||||
import { MCPConnectionManager } from '@/mcp/connection';
|
import { MCPConnectionManager } from '@/mcp/connection';
|
||||||
|
|
@ -25,15 +26,33 @@ export function MCPProvider({ children }: MCPProviderProps) {
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const hasLoadedPersisted = useRef(false);
|
const hasLoadedPersisted = useRef(false);
|
||||||
|
const connectionsRef = useRef<MCPConnection[]>([]);
|
||||||
|
|
||||||
// Message callback management
|
// Message callback management
|
||||||
const [messageCallbacks, setMessageCallbacks] = useState<Map<string, MCPMessageCallback>>(new Map());
|
const [messageCallbacks, setMessageCallbacks] = useState<Map<string, MCPMessageCallback>>(new Map());
|
||||||
const messageCallbacksRef = useRef<Map<string, MCPMessageCallback>>(new Map());
|
const messageCallbacksRef = useRef<Map<string, MCPMessageCallback>>(new Map());
|
||||||
|
|
||||||
// Keep ref in sync with state
|
// Store messages for persistence
|
||||||
|
const [messages, setMessages] = useState<MCPMonitorMessage[]>([]);
|
||||||
|
const maxMessages = 100; // Keep last 100 messages
|
||||||
|
|
||||||
|
// Keep refs in sync with state
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
messageCallbacksRef.current = messageCallbacks;
|
messageCallbacksRef.current = messageCallbacks;
|
||||||
}, [messageCallbacks]);
|
}, [messageCallbacks]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
connectionsRef.current = connections;
|
||||||
|
}, [connections]);
|
||||||
|
|
||||||
|
// Function to add a message to the stored messages
|
||||||
|
const addMessage = useCallback((message: MCPMonitorMessage) => {
|
||||||
|
setMessages(prev => {
|
||||||
|
const newMessages = [...prev, message];
|
||||||
|
// Keep only the latest maxMessages
|
||||||
|
return newMessages.slice(-maxMessages);
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
// Function to broadcast messages to all callbacks (stable reference)
|
// Function to broadcast messages to all callbacks (stable reference)
|
||||||
const broadcastMessage = useCallback((
|
const broadcastMessage = useCallback((
|
||||||
|
|
@ -43,7 +62,24 @@ export function MCPProvider({ children }: MCPProviderProps) {
|
||||||
direction: 'sent' | 'received',
|
direction: 'sent' | 'received',
|
||||||
extra?: any
|
extra?: any
|
||||||
) => {
|
) => {
|
||||||
console.log(`MCP Message [${direction}] from ${connectionId}:`, message); // Debug log
|
// console.log(`MCP Message [${direction}] from ${connectionId}:`, message); // Debug log
|
||||||
|
|
||||||
|
// Find connection name using ref to get latest state
|
||||||
|
const connection = connectionsRef.current.find(conn => conn.id === connectionId);
|
||||||
|
const serverName = connection?.name || 'Unknown Server';
|
||||||
|
|
||||||
|
// Store message
|
||||||
|
const storedMessage: MCPMonitorMessage = {
|
||||||
|
id: uuidv4(),
|
||||||
|
timestamp: new Date(),
|
||||||
|
connectionId,
|
||||||
|
serverName,
|
||||||
|
message,
|
||||||
|
direction,
|
||||||
|
extra,
|
||||||
|
};
|
||||||
|
addMessage(storedMessage);
|
||||||
|
|
||||||
messageCallbacksRef.current.forEach(callback => {
|
messageCallbacksRef.current.forEach(callback => {
|
||||||
try {
|
try {
|
||||||
callback(connectionId, client, message, direction, extra);
|
callback(connectionId, client, message, direction, extra);
|
||||||
|
|
@ -51,7 +87,7 @@ export function MCPProvider({ children }: MCPProviderProps) {
|
||||||
console.error('Error in MCP message callback:', error);
|
console.error('Error in MCP message callback:', error);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}, []); // No dependencies - uses ref
|
}, [addMessage]); // Only depends on addMessage, uses ref for connections
|
||||||
|
|
||||||
// Load persisted connections from localStorage on mount
|
// Load persisted connections from localStorage on mount
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -384,6 +420,10 @@ export function MCPProvider({ children }: MCPProviderProps) {
|
||||||
return newMap;
|
return newMap;
|
||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const clearMessages = useCallback(() => {
|
||||||
|
setMessages([]);
|
||||||
|
}, []);
|
||||||
|
|
||||||
const contextValue: MCPContextValue = {
|
const contextValue: MCPContextValue = {
|
||||||
connections,
|
connections,
|
||||||
|
|
@ -402,8 +442,10 @@ export function MCPProvider({ children }: MCPProviderProps) {
|
||||||
getConnectionById,
|
getConnectionById,
|
||||||
updateServerConfig,
|
updateServerConfig,
|
||||||
handleOAuthCallback,
|
handleOAuthCallback,
|
||||||
|
messages,
|
||||||
addMessageCallback,
|
addMessageCallback,
|
||||||
removeMessageCallback,
|
removeMessageCallback,
|
||||||
|
clearMessages,
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
||||||
|
|
@ -126,7 +126,7 @@ const DEFAULT_CONFIG: AgentLoopConfig = {
|
||||||
|
|
||||||
export function useAgentLoop(config: Partial<AgentLoopConfig> = {}): UseAgentLoopReturn {
|
export function useAgentLoop(config: Partial<AgentLoopConfig> = {}): UseAgentLoopReturn {
|
||||||
const finalConfig = { ...DEFAULT_CONFIG, ...config };
|
const finalConfig = { ...DEFAULT_CONFIG, ...config };
|
||||||
const { provider: currentProvider, isAuthenticated } = useInference();
|
const { provider: currentProvider, isAuthenticated, generateResponse } = useInference();
|
||||||
const { getAllTools, callTool: callMCPTool, connections } = useMCP();
|
const { getAllTools, callTool: callMCPTool, connections } = useMCP();
|
||||||
|
|
||||||
// Track running loops
|
// Track running loops
|
||||||
|
|
@ -309,7 +309,7 @@ export function useAgentLoop(config: Partial<AgentLoopConfig> = {}): UseAgentLoo
|
||||||
temperature: finalConfig.temperature,
|
temperature: finalConfig.temperature,
|
||||||
};
|
};
|
||||||
|
|
||||||
const response = await currentProvider.generateResponse(request);
|
const response = await generateResponse(request);
|
||||||
|
|
||||||
if (abortController.signal.aborted) {
|
if (abortController.signal.aborted) {
|
||||||
break;
|
break;
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,22 @@
|
||||||
// Core inference types and interfaces
|
// Core inference types and interfaces
|
||||||
|
|
||||||
|
// Inference message monitoring types
|
||||||
|
export interface InferenceMessage {
|
||||||
|
id: string;
|
||||||
|
timestamp: Date;
|
||||||
|
type: 'request' | 'response' | 'error' | 'stream_chunk';
|
||||||
|
providerId: string;
|
||||||
|
providerName: string;
|
||||||
|
model?: string;
|
||||||
|
request?: InferenceRequest;
|
||||||
|
response?: InferenceResponse;
|
||||||
|
streamChunk?: any;
|
||||||
|
error?: any;
|
||||||
|
duration?: number; // Response time in ms
|
||||||
|
}
|
||||||
|
|
||||||
|
export type InferenceMessageCallback = (message: InferenceMessage) => void;
|
||||||
|
|
||||||
export interface Model {
|
export interface Model {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
|
|
@ -49,6 +66,8 @@ export interface Tool {
|
||||||
|
|
||||||
export interface InferenceRequest {
|
export interface InferenceRequest {
|
||||||
messages: ChatMessage[];
|
messages: ChatMessage[];
|
||||||
|
model?: string;
|
||||||
|
systemPrompt?: string;
|
||||||
tools?: Tool[]; // toolChoice defaults to 'auto' when tools provided
|
tools?: Tool[]; // toolChoice defaults to 'auto' when tools provided
|
||||||
maxTokens?: number;
|
maxTokens?: number;
|
||||||
temperature?: number;
|
temperature?: number;
|
||||||
|
|
@ -59,6 +78,7 @@ export interface InferenceResponse {
|
||||||
message: ChatMessage;
|
message: ChatMessage;
|
||||||
usage: TokenUsage;
|
usage: TokenUsage;
|
||||||
stopReason: 'stop' | 'max_tokens' | 'tool_calls' | 'error';
|
stopReason: 'stop' | 'max_tokens' | 'tool_calls' | 'error';
|
||||||
|
finishReason?: string;
|
||||||
error?: string;
|
error?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -41,6 +41,17 @@ export type MCPMessageCallback = (
|
||||||
extra?: { authInfo?: AuthInfo; options?: TransportSendOptions }
|
extra?: { authInfo?: AuthInfo; options?: TransportSendOptions }
|
||||||
) => void;
|
) => void;
|
||||||
|
|
||||||
|
// MCP message for storage/monitoring
|
||||||
|
export interface MCPMonitorMessage {
|
||||||
|
id: string;
|
||||||
|
timestamp: Date;
|
||||||
|
connectionId: string;
|
||||||
|
serverName: string;
|
||||||
|
message: JSONRPCMessage;
|
||||||
|
direction: 'sent' | 'received';
|
||||||
|
extra?: { authInfo?: AuthInfo; options?: TransportSendOptions };
|
||||||
|
}
|
||||||
|
|
||||||
export interface MCPResource {
|
export interface MCPResource {
|
||||||
uri: string;
|
uri: string;
|
||||||
name: string;
|
name: string;
|
||||||
|
|
@ -143,6 +154,8 @@ export interface MCPContextValue {
|
||||||
handleOAuthCallback: (connectionId: string, authorizationCode: string) => Promise<void>;
|
handleOAuthCallback: (connectionId: string, authorizationCode: string) => Promise<void>;
|
||||||
|
|
||||||
// Message monitoring
|
// Message monitoring
|
||||||
|
messages: MCPMonitorMessage[];
|
||||||
addMessageCallback: (callback: MCPMessageCallback) => string; // Returns callback ID
|
addMessageCallback: (callback: MCPMessageCallback) => string; // Returns callback ID
|
||||||
removeMessageCallback: (callbackId: string) => void;
|
removeMessageCallback: (callbackId: string) => void;
|
||||||
|
clearMessages: () => void;
|
||||||
}
|
}
|
||||||
Loading…
Reference in a new issue