Add tabbed sidebar with MCP server management and resizable interface
- Created tabbed interface with MCP, Conversations, and Inference tabs - Set MCP tab as default tab - Added quick connect button for example server with OAuth auth - Created form to add custom MCP servers with auth type selection - Implemented server management UI with connect/disconnect/remove actions - Made sidebar resizable with drag handle (240px-600px range) - Added comprehensive MCP server status display and tools listing - Created dedicated Inference tab for OpenRouter connection management 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
6605fbcab6
commit
1ab5496e0a
5 changed files with 469 additions and 24 deletions
|
|
@ -1,38 +1,54 @@
|
||||||
// Main conversation application with sidebar and chat interface
|
// Main conversation application with sidebar and chat interface
|
||||||
|
|
||||||
import { useState } from 'react';
|
import { useState, useRef, useCallback } from 'react';
|
||||||
import { ConversationProvider } from '@/contexts/ConversationContext';
|
import { ConversationProvider } from '@/contexts/ConversationContext';
|
||||||
import { ConversationSidebar } from './ConversationSidebar';
|
import { TabbedSidebar } from './TabbedSidebar';
|
||||||
import { ChatInterface } from './ChatInterface';
|
import { ChatInterface } from './ChatInterface';
|
||||||
import { MCPStatus } from './MCPStatus';
|
|
||||||
|
|
||||||
export function ConversationApp() {
|
export function ConversationApp() {
|
||||||
const [showMCPStatus, setShowMCPStatus] = useState(false);
|
const [sidebarWidth, setSidebarWidth] = useState(320);
|
||||||
|
const isResizing = useRef(false);
|
||||||
|
|
||||||
|
const handleMouseDown = useCallback(() => {
|
||||||
|
isResizing.current = true;
|
||||||
|
document.addEventListener('mousemove', handleMouseMove);
|
||||||
|
document.addEventListener('mouseup', handleMouseUp);
|
||||||
|
document.body.style.cursor = 'col-resize';
|
||||||
|
document.body.style.userSelect = 'none';
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleMouseMove = useCallback((e: MouseEvent) => {
|
||||||
|
if (!isResizing.current) return;
|
||||||
|
|
||||||
|
const newWidth = e.clientX;
|
||||||
|
if (newWidth >= 240 && newWidth <= 600) {
|
||||||
|
setSidebarWidth(newWidth);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleMouseUp = useCallback(() => {
|
||||||
|
isResizing.current = false;
|
||||||
|
document.removeEventListener('mousemove', handleMouseMove);
|
||||||
|
document.removeEventListener('mouseup', handleMouseUp);
|
||||||
|
document.body.style.cursor = '';
|
||||||
|
document.body.style.userSelect = '';
|
||||||
|
}, [handleMouseMove]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ConversationProvider>
|
<ConversationProvider>
|
||||||
<div className="h-screen flex bg-gray-100 dark:bg-gray-900">
|
<div className="h-screen flex bg-gray-100 dark:bg-gray-900">
|
||||||
{/* Left Sidebar - Conversations */}
|
{/* Left Sidebar - Tabbed Interface */}
|
||||||
<div className="w-80 bg-white dark:bg-gray-800 border-r border-gray-200 dark:border-gray-700 flex flex-col">
|
<div
|
||||||
<div className="p-4 border-b border-gray-200 dark:border-gray-700">
|
className="bg-white dark:bg-gray-800 border-r border-gray-200 dark:border-gray-700 flex flex-col relative"
|
||||||
<div className="flex items-center justify-between">
|
style={{ width: `${sidebarWidth}px` }}
|
||||||
<h1 className="text-lg font-semibold text-gray-900 dark:text-white">
|
>
|
||||||
Conversations
|
<TabbedSidebar />
|
||||||
</h1>
|
|
||||||
<button
|
|
||||||
onClick={() => setShowMCPStatus(!showMCPStatus)}
|
|
||||||
className="text-sm px-3 py-1 rounded-md bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-600"
|
|
||||||
>
|
|
||||||
{showMCPStatus ? 'Hide' : 'Show'} MCP
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{showMCPStatus ? (
|
{/* Resize Handle */}
|
||||||
<MCPStatus />
|
<div
|
||||||
) : (
|
className="absolute top-0 right-0 w-1 h-full cursor-col-resize hover:bg-blue-500 hover:bg-opacity-50 transition-colors"
|
||||||
<ConversationSidebar />
|
onMouseDown={handleMouseDown}
|
||||||
)}
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Main Chat Area */}
|
{/* Main Chat Area */}
|
||||||
|
|
|
||||||
115
src/components/InferenceTab.tsx
Normal file
115
src/components/InferenceTab.tsx
Normal file
|
|
@ -0,0 +1,115 @@
|
||||||
|
// Inference tab for managing OpenRouter connection
|
||||||
|
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { useInference } from '@/contexts/InferenceContext';
|
||||||
|
import { OpenRouterOAuthProvider } from '@/providers/openrouter/oauth-provider';
|
||||||
|
|
||||||
|
export function InferenceTab() {
|
||||||
|
const { provider, models, clearProvider, setProvider } = useInference();
|
||||||
|
const [isConnecting, setIsConnecting] = useState(false);
|
||||||
|
|
||||||
|
const handleConnect = async () => {
|
||||||
|
setIsConnecting(true);
|
||||||
|
try {
|
||||||
|
const oauthProvider = new OpenRouterOAuthProvider();
|
||||||
|
await oauthProvider.authenticate({ type: 'oauth' });
|
||||||
|
setProvider(oauthProvider);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to connect to OpenRouter:', error);
|
||||||
|
} finally {
|
||||||
|
setIsConnecting(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDisconnect = () => {
|
||||||
|
if (confirm('Are you sure you want to disconnect from OpenRouter? This will clear your authentication.')) {
|
||||||
|
clearProvider();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex-1 overflow-y-auto p-4">
|
||||||
|
<div className="space-y-4">
|
||||||
|
{/* Connection Status */}
|
||||||
|
<div className="bg-gray-50 dark:bg-gray-700 rounded-lg p-3">
|
||||||
|
<h3 className="font-medium text-gray-900 dark:text-white mb-2">
|
||||||
|
Inference Provider
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
{provider ? (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<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>
|
||||||
|
<div className="space-y-1 max-h-64 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>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
264
src/components/MCPTab.tsx
Normal file
264
src/components/MCPTab.tsx
Normal file
|
|
@ -0,0 +1,264 @@
|
||||||
|
// MCP tab with server management and connection functionality
|
||||||
|
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { useMCP } from '@/contexts/MCPContext';
|
||||||
|
|
||||||
|
export function MCPTab() {
|
||||||
|
const { connections, addMcpServer, removeMcpServer, reconnectServer, isLoading } = useMCP();
|
||||||
|
const [showAddForm, setShowAddForm] = useState(false);
|
||||||
|
const [newServerName, setNewServerName] = useState('');
|
||||||
|
const [newServerUrl, setNewServerUrl] = useState('');
|
||||||
|
const [authType, setAuthType] = useState<'none' | 'oauth'>('none');
|
||||||
|
|
||||||
|
const handleAddExampleServer = async () => {
|
||||||
|
try {
|
||||||
|
await addMcpServer({
|
||||||
|
name: 'Example Server',
|
||||||
|
url: 'https://example-server.modelcontextprotocol.io/sse',
|
||||||
|
authType: 'oauth',
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to add example server:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAddCustomServer = async (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (!newServerName.trim() || !newServerUrl.trim()) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await addMcpServer({
|
||||||
|
name: newServerName,
|
||||||
|
url: newServerUrl,
|
||||||
|
authType,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Reset form
|
||||||
|
setNewServerName('');
|
||||||
|
setNewServerUrl('');
|
||||||
|
setAuthType('none');
|
||||||
|
setShowAddForm(false);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to add custom server:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getStatusColor = (status: string) => {
|
||||||
|
switch (status) {
|
||||||
|
case 'connected':
|
||||||
|
return 'text-green-600 dark:text-green-400';
|
||||||
|
case 'connecting':
|
||||||
|
return 'text-yellow-600 dark:text-yellow-400';
|
||||||
|
case 'failed':
|
||||||
|
return 'text-red-600 dark:text-red-400';
|
||||||
|
default:
|
||||||
|
return 'text-gray-600 dark:text-gray-400';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getStatusIcon = (status: string) => {
|
||||||
|
switch (status) {
|
||||||
|
case 'connected':
|
||||||
|
return '🟢';
|
||||||
|
case 'connecting':
|
||||||
|
return '🟡';
|
||||||
|
case 'failed':
|
||||||
|
return '🔴';
|
||||||
|
default:
|
||||||
|
return '⚪';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const connectedServers = connections.filter(conn => conn.status === 'connected');
|
||||||
|
const allTools = connections.flatMap(conn => conn.tools);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex-1 flex flex-col overflow-hidden">
|
||||||
|
{/* Summary */}
|
||||||
|
<div className="p-4 border-b border-gray-200 dark:border-gray-700">
|
||||||
|
<div className="bg-gray-50 dark:bg-gray-700 rounded-lg p-3">
|
||||||
|
<h3 className="font-medium text-gray-900 dark:text-white mb-2">
|
||||||
|
MCP Summary
|
||||||
|
</h3>
|
||||||
|
<div className="text-sm space-y-1">
|
||||||
|
<p className="text-gray-700 dark:text-gray-300">
|
||||||
|
<span className="font-medium">{connectedServers.length}</span> servers connected
|
||||||
|
</p>
|
||||||
|
<p className="text-gray-700 dark:text-gray-300">
|
||||||
|
<span className="font-medium">{allTools.length}</span> tools available
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Add Servers Section */}
|
||||||
|
<div className="p-4 border-b border-gray-200 dark:border-gray-700">
|
||||||
|
<h3 className="font-medium text-gray-900 dark:text-white mb-3">
|
||||||
|
Add Servers
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
{/* Example Server Button */}
|
||||||
|
<button
|
||||||
|
onClick={handleAddExampleServer}
|
||||||
|
disabled={isLoading || connections.some(conn => conn.name === 'Example Server')}
|
||||||
|
className="w-full mb-3 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"
|
||||||
|
>
|
||||||
|
{connections.some(conn => conn.name === 'Example Server') ? 'Example Server Added' : 'Add Example Server'}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* Custom Server Form Toggle */}
|
||||||
|
<button
|
||||||
|
onClick={() => setShowAddForm(!showAddForm)}
|
||||||
|
className="w-full px-4 py-2 text-sm bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300 rounded-lg hover:bg-gray-200 dark:hover:bg-gray-600 transition-colors"
|
||||||
|
>
|
||||||
|
{showAddForm ? 'Cancel' : 'Add Custom Server'}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* Custom Server Form */}
|
||||||
|
{showAddForm && (
|
||||||
|
<form onSubmit={handleAddCustomServer} className="mt-3 space-y-3">
|
||||||
|
<div>
|
||||||
|
<label className="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||||
|
Server Name
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={newServerName}
|
||||||
|
onChange={(e) => setNewServerName(e.target.value)}
|
||||||
|
placeholder="My MCP Server"
|
||||||
|
className="w-full px-3 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||||
|
Server URL
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="url"
|
||||||
|
value={newServerUrl}
|
||||||
|
onChange={(e) => setNewServerUrl(e.target.value)}
|
||||||
|
placeholder="https://your-server.com/sse"
|
||||||
|
className="w-full px-3 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||||
|
Authentication
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
value={authType}
|
||||||
|
onChange={(e) => setAuthType(e.target.value as 'none' | 'oauth')}
|
||||||
|
className="w-full px-3 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||||||
|
>
|
||||||
|
<option value="none">None</option>
|
||||||
|
<option value="oauth">OAuth</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={isLoading || !newServerName.trim() || !newServerUrl.trim()}
|
||||||
|
className="w-full px-4 py-2 text-sm bg-green-600 text-white rounded-lg hover:bg-green-700 disabled:bg-gray-400 disabled:cursor-not-allowed transition-colors"
|
||||||
|
>
|
||||||
|
{isLoading ? 'Adding...' : 'Add Server'}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Server List */}
|
||||||
|
<div className="flex-1 overflow-y-auto p-4">
|
||||||
|
<div className="flex items-center justify-between mb-3">
|
||||||
|
<h3 className="font-medium text-gray-900 dark:text-white">
|
||||||
|
Servers ({connections.length})
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{connections.length === 0 ? (
|
||||||
|
<p className="text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
No MCP servers configured. Add a server above to get started.
|
||||||
|
</p>
|
||||||
|
) : (
|
||||||
|
<div className="space-y-3">
|
||||||
|
{connections.map((connection) => (
|
||||||
|
<div
|
||||||
|
key={connection.id}
|
||||||
|
className="bg-white dark:bg-gray-700 rounded-lg p-3 border border-gray-200 dark:border-gray-600"
|
||||||
|
>
|
||||||
|
<div className="flex items-start justify-between mb-2">
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<span className="text-sm">
|
||||||
|
{getStatusIcon(connection.status)}
|
||||||
|
</span>
|
||||||
|
<h4 className="text-sm font-medium text-gray-900 dark:text-white truncate">
|
||||||
|
{connection.name}
|
||||||
|
</h4>
|
||||||
|
</div>
|
||||||
|
<p className={`text-xs mt-1 ${getStatusColor(connection.status)}`}>
|
||||||
|
{connection.status}
|
||||||
|
</p>
|
||||||
|
{connection.error && (
|
||||||
|
<p className="text-xs text-red-600 dark:text-red-400 mt-1">
|
||||||
|
{connection.error}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1 truncate">
|
||||||
|
{connection.url}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex space-x-1 ml-2">
|
||||||
|
{(connection.status === 'failed' || connection.status === 'disconnected') && (
|
||||||
|
<button
|
||||||
|
onClick={() => reconnectServer(connection.id)}
|
||||||
|
className="px-2 py-1 text-xs bg-blue-100 dark:bg-blue-900/50 text-blue-800 dark:text-blue-200 rounded hover:bg-blue-200 dark:hover:bg-blue-800"
|
||||||
|
title="Reconnect server"
|
||||||
|
>
|
||||||
|
Reconnect
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
<button
|
||||||
|
onClick={() => removeMcpServer(connection.id)}
|
||||||
|
className="px-2 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"
|
||||||
|
title="Remove server"
|
||||||
|
>
|
||||||
|
Remove
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{connection.tools.length > 0 && (
|
||||||
|
<div className="mt-2">
|
||||||
|
<p className="text-xs text-gray-600 dark:text-gray-400 mb-1">
|
||||||
|
Tools ({connection.tools.length}):
|
||||||
|
</p>
|
||||||
|
<div className="flex flex-wrap gap-1">
|
||||||
|
{connection.tools.slice(0, 3).map((tool, index) => (
|
||||||
|
<span
|
||||||
|
key={index}
|
||||||
|
className="inline-block px-2 py-1 bg-blue-100 dark:bg-blue-900/50 text-blue-800 dark:text-blue-200 text-xs rounded"
|
||||||
|
>
|
||||||
|
{tool.function.name.split('__').pop()}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
{connection.tools.length > 3 && (
|
||||||
|
<span className="text-xs text-gray-500 dark:text-gray-400">
|
||||||
|
+{connection.tools.length - 3} more
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
49
src/components/TabbedSidebar.tsx
Normal file
49
src/components/TabbedSidebar.tsx
Normal file
|
|
@ -0,0 +1,49 @@
|
||||||
|
// Tabbed sidebar with Conversations, MCP, and Inference tabs
|
||||||
|
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { ConversationSidebar } from './ConversationSidebar';
|
||||||
|
import { MCPTab } from './MCPTab';
|
||||||
|
import { InferenceTab } from './InferenceTab';
|
||||||
|
|
||||||
|
type TabType = 'mcp' | 'conversations' | 'inference';
|
||||||
|
|
||||||
|
export function TabbedSidebar() {
|
||||||
|
const [activeTab, setActiveTab] = useState<TabType>('mcp');
|
||||||
|
|
||||||
|
const tabs = [
|
||||||
|
{ id: 'mcp' as const, label: 'MCP', icon: '🔧' },
|
||||||
|
{ id: 'conversations' as const, label: 'Conversations', icon: '💬' },
|
||||||
|
{ id: 'inference' as const, label: 'Inference', icon: '🤖' },
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex-1 flex flex-col">
|
||||||
|
{/* Tab Headers */}
|
||||||
|
<div className="border-b border-gray-200 dark:border-gray-700">
|
||||||
|
<div className="flex">
|
||||||
|
{tabs.map((tab) => (
|
||||||
|
<button
|
||||||
|
key={tab.id}
|
||||||
|
onClick={() => setActiveTab(tab.id)}
|
||||||
|
className={`flex-1 px-4 py-3 text-sm font-medium transition-colors ${
|
||||||
|
activeTab === tab.id
|
||||||
|
? 'text-blue-600 dark:text-blue-400 border-b-2 border-blue-600 dark:border-blue-400 bg-blue-50 dark:bg-blue-900/20'
|
||||||
|
: 'text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700/50'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<span className="mr-2">{tab.icon}</span>
|
||||||
|
{tab.label}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Tab Content */}
|
||||||
|
<div className="flex-1 overflow-hidden">
|
||||||
|
{activeTab === 'mcp' && <MCPTab />}
|
||||||
|
{activeTab === 'conversations' && <ConversationSidebar />}
|
||||||
|
{activeTab === 'inference' && <InferenceTab />}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -522,6 +522,7 @@ export class MCPConnectionManager {
|
||||||
try {
|
try {
|
||||||
console.log('Initializing MCP client...');
|
console.log('Initializing MCP client...');
|
||||||
this.transport = transport;
|
this.transport = transport;
|
||||||
|
this.transport.onmessage = console.log.bind(console, 'MCP Client message received:');
|
||||||
this.client = new Client(
|
this.client = new Client(
|
||||||
{
|
{
|
||||||
name: 'example-remote-client',
|
name: 'example-remote-client',
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue