BusyBee3333 4e6467ffb0 Add CRESync CRM application with Setup page
- Build complete Next.js CRM for commercial real estate
- Add authentication with JWT sessions and role-based access
- Add GoHighLevel API integration for contacts, conversations, opportunities
- Add AI-powered Control Center with tool calling
- Add Setup page with onboarding checklist (/setup)
- Add sidebar navigation with Setup menu item
- Fix type errors in onboarding API, GHL services, and control center tools
- Add Prisma schema with SQLite for local development
- Add UI components with clay morphism design system

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-14 17:30:55 -05:00

298 lines
9.3 KiB
TypeScript

'use client';
import React, { useEffect, useState, useCallback } from 'react';
import { Menu, X, Loader2, AlertCircle } from 'lucide-react';
import { cn } from '@/lib/utils';
import { useIsMobile } from '@/hooks/use-mobile';
import {
ChatProvider,
useChatContext,
ChatInterface,
ConversationSidebar,
StatusIndicator,
} from '@/components/control-center';
import type { ControlCenterConversation } from '@/types/control-center';
// =============================================================================
// Types
// =============================================================================
interface MCPStatus {
connected: boolean;
toolCount: number;
error?: string;
}
interface ToolsResponse {
tools: unknown[];
mcpStatus: MCPStatus;
appToolCount: number;
}
// =============================================================================
// Inner Component (uses ChatContext)
// =============================================================================
function ControlCenterContent() {
const isMobile = useIsMobile();
const [sidebarOpen, setSidebarOpen] = useState(false);
const [mcpStatus, setMcpStatus] = useState<MCPStatus>({
connected: false,
toolCount: 0,
});
const [initialLoading, setInitialLoading] = useState(true);
const [toolsError, setToolsError] = useState<string | null>(null);
const {
conversations,
currentConversation,
loadConversations,
selectConversation,
newConversation,
isLoading,
isStreaming,
error,
} = useChatContext();
// Determine connection status for StatusIndicator
const getConnectionStatus = (): 'connected' | 'connecting' | 'error' | 'idle' => {
if (error || toolsError) return 'error';
if (isLoading || isStreaming) return 'connecting';
if (mcpStatus.connected) return 'connected';
return 'idle';
};
// Fetch available tools on mount to check MCP status
const fetchTools = useCallback(async () => {
try {
const response = await fetch('/api/v1/control-center/tools');
if (!response.ok) {
throw new Error('Failed to fetch tools');
}
const data: ToolsResponse = await response.json();
setMcpStatus(data.mcpStatus);
setToolsError(null);
} catch (err) {
const message = err instanceof Error ? err.message : 'Failed to check MCP status';
setToolsError(message);
setMcpStatus({ connected: false, toolCount: 0, error: message });
}
}, []);
// Load initial data on mount
useEffect(() => {
const initializeData = async () => {
setInitialLoading(true);
try {
await Promise.all([loadConversations(), fetchTools()]);
} finally {
setInitialLoading(false);
}
};
initializeData();
}, [loadConversations, fetchTools]);
// Close sidebar when selecting a conversation on mobile
const handleSelectConversation = useCallback(
async (id: string) => {
await selectConversation(id);
if (isMobile) {
setSidebarOpen(false);
}
},
[selectConversation, isMobile]
);
// Handle new conversation
const handleNewConversation = useCallback(() => {
newConversation();
if (isMobile) {
setSidebarOpen(false);
}
}, [newConversation, isMobile]);
// Convert ConversationSummary[] to ControlCenterConversation[] for sidebar
// The sidebar expects ControlCenterConversation but we only have summaries
const conversationsForSidebar: ControlCenterConversation[] = conversations.map((conv) => ({
id: conv.id,
title: conv.title,
messages: [],
createdAt: conv.createdAt,
updatedAt: conv.updatedAt,
}));
// Show loading state while initial data is being fetched
if (initialLoading) {
return (
<div className="flex items-center justify-center h-full">
<div className="flex flex-col items-center gap-4">
<div
className={cn(
'w-16 h-16 rounded-2xl flex items-center justify-center',
'bg-[#F0F4F8]',
'shadow-[6px_6px_12px_#bfc3cc,-6px_-6px_12px_#ffffff]'
)}
>
<Loader2 className="w-8 h-8 text-indigo-500 animate-spin" />
</div>
<p className="text-gray-500 font-medium">Loading Control Center...</p>
</div>
</div>
);
}
return (
<div className="h-[calc(100vh-10rem)] flex flex-col">
{/* Mobile Header with Sidebar Toggle */}
<div className="lg:hidden flex items-center justify-between mb-4 px-1 flex-shrink-0">
<button
onClick={() => setSidebarOpen(true)}
className={cn(
'flex items-center gap-2 px-4 py-2.5',
'bg-[#F0F4F8]',
'rounded-xl',
'shadow-[4px_4px_8px_#bfc3cc,-4px_-4px_8px_#ffffff]',
'hover:shadow-[2px_2px_4px_#bfc3cc,-2px_-2px_4px_#ffffff]',
'active:shadow-[inset_2px_2px_4px_rgba(0,0,0,0.05)]',
'transition-all duration-200'
)}
aria-label="Open conversation history"
>
<Menu className="w-5 h-5 text-gray-600" />
<span className="text-sm font-medium text-gray-700">History</span>
</button>
<StatusIndicator
status={getConnectionStatus()}
mcpConnected={mcpStatus.connected}
/>
</div>
{/* Desktop Layout */}
<div className="hidden lg:grid lg:grid-cols-[280px_1fr] gap-6 flex-1 min-h-0 overflow-hidden">
{/* Left Sidebar */}
<div className="flex flex-col min-h-0 overflow-hidden">
{/* Status Indicator */}
<div className="mb-4 flex justify-center">
<StatusIndicator
status={getConnectionStatus()}
mcpConnected={mcpStatus.connected}
/>
</div>
{/* Conversation Sidebar */}
<div className="flex-1 min-h-0">
<ConversationSidebar
conversations={conversationsForSidebar}
currentId={currentConversation?.id}
onSelect={handleSelectConversation}
onNew={handleNewConversation}
/>
</div>
</div>
{/* Main Chat Area */}
<div className="min-h-0 overflow-hidden">
<ChatInterface className="h-full" />
</div>
</div>
{/* Mobile Layout */}
<div className="lg:hidden flex-1 min-h-0 overflow-hidden">
<ChatInterface className="h-full" />
</div>
{/* Mobile Sidebar Overlay */}
{sidebarOpen && (
<div
className="lg:hidden fixed inset-0 z-50"
role="dialog"
aria-modal="true"
aria-label="Conversation history"
>
{/* Backdrop */}
<div
className="absolute inset-0 bg-black/40 backdrop-blur-sm"
onClick={() => setSidebarOpen(false)}
aria-hidden="true"
/>
{/* Sidebar Panel */}
<div
className={cn(
'absolute left-0 top-0 h-full w-[280px]',
'bg-[#F0F4F8]',
'shadow-[8px_0_24px_rgba(0,0,0,0.15)]',
'animate-slide-in-from-left'
)}
>
{/* Close button */}
<div className="flex items-center justify-between p-4 border-b border-gray-200/50">
<h2 className="font-semibold text-gray-800">Conversation History</h2>
<button
onClick={() => setSidebarOpen(false)}
className={cn(
'p-2 rounded-lg',
'text-gray-500 hover:text-gray-700',
'hover:bg-gray-100',
'transition-colors'
)}
aria-label="Close sidebar"
>
<X className="w-5 h-5" />
</button>
</div>
{/* Sidebar content */}
<div className="h-[calc(100%-65px)]">
<ConversationSidebar
conversations={conversationsForSidebar}
currentId={currentConversation?.id}
onSelect={handleSelectConversation}
onNew={handleNewConversation}
/>
</div>
</div>
</div>
)}
{/* MCP Warning Banner (shown at bottom on desktop if MCP not connected) */}
{!mcpStatus.connected && mcpStatus.error && !initialLoading && (
<div className="hidden lg:block fixed bottom-6 left-1/2 -translate-x-1/2 z-40">
<div
className={cn(
'flex items-center gap-3 px-4 py-3',
'bg-amber-50 border border-amber-200',
'rounded-xl shadow-lg',
'max-w-md'
)}
>
<AlertCircle className="w-5 h-5 text-amber-600 flex-shrink-0" />
<div>
<p className="text-sm font-medium text-amber-800">
Limited Functionality
</p>
<p className="text-xs text-amber-600">
{mcpStatus.error || 'MCP server not connected. Some tools may be unavailable.'}
</p>
</div>
</div>
</div>
)}
</div>
);
}
// =============================================================================
// Main Page Component
// =============================================================================
export default function ControlCenterPage() {
return (
<ChatProvider>
<ControlCenterContent />
</ChatProvider>
);
}