Skip to content

Commit 1ab5496

Browse files
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 <[email protected]>
1 parent 6605fbc commit 1ab5496

File tree

5 files changed

+469
-24
lines changed

5 files changed

+469
-24
lines changed

src/components/ConversationApp.tsx

Lines changed: 40 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,54 @@
11
// Main conversation application with sidebar and chat interface
22

3-
import { useState } from 'react';
3+
import { useState, useRef, useCallback } from 'react';
44
import { ConversationProvider } from '@/contexts/ConversationContext';
5-
import { ConversationSidebar } from './ConversationSidebar';
5+
import { TabbedSidebar } from './TabbedSidebar';
66
import { ChatInterface } from './ChatInterface';
7-
import { MCPStatus } from './MCPStatus';
87

98
export function ConversationApp() {
10-
const [showMCPStatus, setShowMCPStatus] = useState(false);
9+
const [sidebarWidth, setSidebarWidth] = useState(320);
10+
const isResizing = useRef(false);
11+
12+
const handleMouseDown = useCallback(() => {
13+
isResizing.current = true;
14+
document.addEventListener('mousemove', handleMouseMove);
15+
document.addEventListener('mouseup', handleMouseUp);
16+
document.body.style.cursor = 'col-resize';
17+
document.body.style.userSelect = 'none';
18+
}, []);
19+
20+
const handleMouseMove = useCallback((e: MouseEvent) => {
21+
if (!isResizing.current) return;
22+
23+
const newWidth = e.clientX;
24+
if (newWidth >= 240 && newWidth <= 600) {
25+
setSidebarWidth(newWidth);
26+
}
27+
}, []);
28+
29+
const handleMouseUp = useCallback(() => {
30+
isResizing.current = false;
31+
document.removeEventListener('mousemove', handleMouseMove);
32+
document.removeEventListener('mouseup', handleMouseUp);
33+
document.body.style.cursor = '';
34+
document.body.style.userSelect = '';
35+
}, [handleMouseMove]);
1136

1237
return (
1338
<ConversationProvider>
1439
<div className="h-screen flex bg-gray-100 dark:bg-gray-900">
15-
{/* Left Sidebar - Conversations */}
16-
<div className="w-80 bg-white dark:bg-gray-800 border-r border-gray-200 dark:border-gray-700 flex flex-col">
17-
<div className="p-4 border-b border-gray-200 dark:border-gray-700">
18-
<div className="flex items-center justify-between">
19-
<h1 className="text-lg font-semibold text-gray-900 dark:text-white">
20-
Conversations
21-
</h1>
22-
<button
23-
onClick={() => setShowMCPStatus(!showMCPStatus)}
24-
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"
25-
>
26-
{showMCPStatus ? 'Hide' : 'Show'} MCP
27-
</button>
28-
</div>
29-
</div>
40+
{/* Left Sidebar - Tabbed Interface */}
41+
<div
42+
className="bg-white dark:bg-gray-800 border-r border-gray-200 dark:border-gray-700 flex flex-col relative"
43+
style={{ width: `${sidebarWidth}px` }}
44+
>
45+
<TabbedSidebar />
3046

31-
{showMCPStatus ? (
32-
<MCPStatus />
33-
) : (
34-
<ConversationSidebar />
35-
)}
47+
{/* Resize Handle */}
48+
<div
49+
className="absolute top-0 right-0 w-1 h-full cursor-col-resize hover:bg-blue-500 hover:bg-opacity-50 transition-colors"
50+
onMouseDown={handleMouseDown}
51+
/>
3652
</div>
3753

3854
{/* Main Chat Area */}

src/components/InferenceTab.tsx

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
// Inference tab for managing OpenRouter connection
2+
3+
import { useState } from 'react';
4+
import { useInference } from '@/contexts/InferenceContext';
5+
import { OpenRouterOAuthProvider } from '@/providers/openrouter/oauth-provider';
6+
7+
export function InferenceTab() {
8+
const { provider, models, clearProvider, setProvider } = useInference();
9+
const [isConnecting, setIsConnecting] = useState(false);
10+
11+
const handleConnect = async () => {
12+
setIsConnecting(true);
13+
try {
14+
const oauthProvider = new OpenRouterOAuthProvider();
15+
await oauthProvider.authenticate({ type: 'oauth' });
16+
setProvider(oauthProvider);
17+
} catch (error) {
18+
console.error('Failed to connect to OpenRouter:', error);
19+
} finally {
20+
setIsConnecting(false);
21+
}
22+
};
23+
24+
const handleDisconnect = () => {
25+
if (confirm('Are you sure you want to disconnect from OpenRouter? This will clear your authentication.')) {
26+
clearProvider();
27+
}
28+
};
29+
30+
return (
31+
<div className="flex-1 overflow-y-auto p-4">
32+
<div className="space-y-4">
33+
{/* Connection Status */}
34+
<div className="bg-gray-50 dark:bg-gray-700 rounded-lg p-3">
35+
<h3 className="font-medium text-gray-900 dark:text-white mb-2">
36+
Inference Provider
37+
</h3>
38+
39+
{provider ? (
40+
<div className="space-y-2">
41+
<div className="flex items-center justify-between">
42+
<div>
43+
<p className="text-sm text-gray-700 dark:text-gray-300">
44+
<span className="text-green-600 dark:text-green-400">🟢</span> Connected to OpenRouter
45+
</p>
46+
<p className="text-xs text-gray-500 dark:text-gray-400">
47+
{models.length} models available
48+
</p>
49+
</div>
50+
<button
51+
onClick={handleDisconnect}
52+
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"
53+
>
54+
Disconnect
55+
</button>
56+
</div>
57+
</div>
58+
) : (
59+
<div className="space-y-2">
60+
<p className="text-sm text-gray-700 dark:text-gray-300">
61+
<span className="text-gray-400"></span> Not connected
62+
</p>
63+
<button
64+
onClick={handleConnect}
65+
disabled={isConnecting}
66+
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"
67+
>
68+
{isConnecting ? 'Connecting...' : 'Connect to OpenRouter'}
69+
</button>
70+
</div>
71+
)}
72+
</div>
73+
74+
{/* Available Models */}
75+
{provider && models.length > 0 && (
76+
<div>
77+
<h3 className="font-medium text-gray-900 dark:text-white mb-3">
78+
Available Models ({models.length})
79+
</h3>
80+
<div className="space-y-1 max-h-64 overflow-y-auto">
81+
{models.map((model) => (
82+
<div
83+
key={model.id}
84+
className="text-xs bg-gray-50 dark:bg-gray-700 rounded px-2 py-2"
85+
>
86+
<div className="font-mono text-gray-900 dark:text-white">
87+
{model.id}
88+
</div>
89+
{model.name && model.name !== model.id && (
90+
<div className="text-gray-600 dark:text-gray-300 mt-1">
91+
{model.name}
92+
</div>
93+
)}
94+
<div className="text-gray-500 dark:text-gray-400 mt-1">
95+
Context: {model.contextLength?.toLocaleString() || 'Unknown'}
96+
</div>
97+
</div>
98+
))}
99+
</div>
100+
</div>
101+
)}
102+
103+
{/* Help Text */}
104+
<div className="text-xs text-gray-500 dark:text-gray-400 space-y-1">
105+
<p>
106+
Connect to OpenRouter to access AI models for conversations.
107+
</p>
108+
<p>
109+
You'll need an OpenRouter account and API key to authenticate.
110+
</p>
111+
</div>
112+
</div>
113+
</div>
114+
);
115+
}

0 commit comments

Comments
 (0)