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

201 lines
6.1 KiB
TypeScript

'use client';
import React, { useEffect, useRef } from 'react';
import { useChatContext } from './ChatProvider';
import { AIMessageBubble } from './AIMessageBubble';
import { UserMessageBubble } from './UserMessageBubble';
import { cn } from '@/lib/utils';
import type { ControlCenterMessage } from '@/types/control-center';
// =============================================================================
// Types
// =============================================================================
interface MessageListProps {
className?: string;
}
// =============================================================================
// System Message Component
// =============================================================================
interface SystemMessageProps {
message: ControlCenterMessage;
}
function SystemMessage({ message }: SystemMessageProps) {
return (
<div className="flex justify-center mb-4">
<div
className={cn(
'max-w-[90%] rounded-xl px-4 py-2',
'bg-gray-100 text-gray-500',
'border border-gray-200',
'text-xs text-center'
)}
>
{message.content}
</div>
</div>
);
}
// =============================================================================
// Empty State Component
// =============================================================================
function EmptyState() {
return (
<div className="flex flex-col items-center justify-center h-full text-center px-6 py-12">
<div
className={cn(
'w-20 h-20 rounded-full flex items-center justify-center mb-6',
'bg-gradient-to-br from-indigo-100 to-purple-100',
'shadow-[6px_6px_12px_#bfc3cc,-6px_-6px_12px_#ffffff]'
)}
>
<svg
className="w-10 h-10 text-indigo-500"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={1.5}
d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z"
/>
</svg>
</div>
<h3 className="text-lg font-semibold text-gray-700 mb-2">
Start a Conversation
</h3>
<p className="text-sm text-gray-500 max-w-sm">
Ask me anything about your CRM data, create automations, or get help
managing your commercial real estate business.
</p>
</div>
);
}
// =============================================================================
// Loading Indicator
// =============================================================================
function LoadingIndicator() {
return (
<div className="flex justify-start mb-4">
<div
className={cn(
'rounded-2xl rounded-bl-md px-4 py-3',
'bg-[#F0F4F8]',
'shadow-[4px_4px_8px_#d1d5db,-4px_-4px_8px_#ffffff]',
'border border-gray-100'
)}
>
<div className="flex items-center gap-1">
<span className="w-2 h-2 bg-indigo-400 rounded-full animate-bounce" style={{ animationDelay: '0ms' }} />
<span className="w-2 h-2 bg-indigo-400 rounded-full animate-bounce" style={{ animationDelay: '150ms' }} />
<span className="w-2 h-2 bg-indigo-400 rounded-full animate-bounce" style={{ animationDelay: '300ms' }} />
</div>
</div>
</div>
);
}
// =============================================================================
// Main Component
// =============================================================================
export function MessageList({ className }: MessageListProps) {
const {
currentConversation,
isLoading,
isStreaming,
streamingContent,
} = useChatContext();
const scrollRef = useRef<HTMLDivElement>(null);
const messagesEndRef = useRef<HTMLDivElement>(null);
// Auto-scroll to bottom when new messages arrive or streaming updates
useEffect(() => {
if (messagesEndRef.current) {
messagesEndRef.current.scrollIntoView({ behavior: 'smooth' });
}
}, [currentConversation?.messages, streamingContent]);
const messages = currentConversation?.messages || [];
const hasMessages = messages.length > 0 || isStreaming || streamingContent;
return (
<div
ref={scrollRef}
className={cn(
'flex-1 overflow-y-auto',
className
)}
>
<div className="p-4 md:p-6">
{!hasMessages && !isLoading ? (
<EmptyState />
) : (
<div className="space-y-4">
{messages.map((message) => {
switch (message.role) {
case 'user':
return (
<UserMessageBubble
key={message.id}
message={message}
/>
);
case 'assistant':
return (
<AIMessageBubble
key={message.id}
message={message}
/>
);
case 'system':
return (
<SystemMessage
key={message.id}
message={message}
/>
);
default:
return null;
}
})}
{/* Show streaming content */}
{isStreaming && streamingContent && (
<AIMessageBubble
message={{
id: 'streaming',
role: 'assistant',
content: streamingContent,
createdAt: new Date().toISOString(),
}}
isStreaming={true}
/>
)}
{/* Show loading indicator when waiting for response but not yet streaming */}
{isLoading && !isStreaming && !streamingContent && (
<LoadingIndicator />
)}
{/* Scroll anchor */}
<div ref={messagesEndRef} />
</div>
)}
</div>
</div>
);
}
export default MessageList;