- 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>
201 lines
6.1 KiB
TypeScript
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;
|