- 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>
207 lines
5.5 KiB
TypeScript
207 lines
5.5 KiB
TypeScript
'use client';
|
|
|
|
import React, { useCallback } from 'react';
|
|
import { cn } from '@/lib/utils';
|
|
import { useChatContext } from './ChatProvider';
|
|
import { MessageList } from './MessageList';
|
|
import { ChatComposer } from './ChatComposer';
|
|
import { Sparkles, AlertCircle, X } from 'lucide-react';
|
|
|
|
// =============================================================================
|
|
// Types
|
|
// =============================================================================
|
|
|
|
interface ChatInterfaceProps {
|
|
className?: string;
|
|
}
|
|
|
|
// =============================================================================
|
|
// Status Indicator Component
|
|
// =============================================================================
|
|
|
|
interface StatusIndicatorProps {
|
|
status: 'idle' | 'loading' | 'streaming' | 'error';
|
|
}
|
|
|
|
function StatusIndicator({ status }: StatusIndicatorProps) {
|
|
const statusConfig = {
|
|
idle: {
|
|
color: 'bg-emerald-400',
|
|
pulse: false,
|
|
label: 'Ready',
|
|
},
|
|
loading: {
|
|
color: 'bg-amber-400',
|
|
pulse: true,
|
|
label: 'Loading',
|
|
},
|
|
streaming: {
|
|
color: 'bg-indigo-400',
|
|
pulse: true,
|
|
label: 'Responding',
|
|
},
|
|
error: {
|
|
color: 'bg-red-400',
|
|
pulse: false,
|
|
label: 'Error',
|
|
},
|
|
};
|
|
|
|
const config = statusConfig[status];
|
|
|
|
return (
|
|
<div className="flex items-center gap-2">
|
|
<span
|
|
className={cn(
|
|
'w-2.5 h-2.5 rounded-full',
|
|
config.color,
|
|
config.pulse && 'animate-pulse'
|
|
)}
|
|
/>
|
|
<span className="text-xs text-gray-500 font-medium">{config.label}</span>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// =============================================================================
|
|
// Error Banner Component
|
|
// =============================================================================
|
|
|
|
interface ErrorBannerProps {
|
|
message: string;
|
|
onDismiss: () => void;
|
|
}
|
|
|
|
function ErrorBanner({ message, onDismiss }: ErrorBannerProps) {
|
|
return (
|
|
<div
|
|
className={cn(
|
|
'flex items-center gap-3 px-4 py-3 mx-4 mt-4 rounded-xl',
|
|
'bg-red-50 border border-red-200',
|
|
'shadow-[inset_2px_2px_4px_rgba(0,0,0,0.03),inset_-2px_-2px_4px_rgba(255,255,255,0.5)]'
|
|
)}
|
|
>
|
|
<AlertCircle className="w-5 h-5 text-red-500 flex-shrink-0" />
|
|
<p className="flex-1 text-sm text-red-700">{message}</p>
|
|
<button
|
|
onClick={onDismiss}
|
|
className={cn(
|
|
'p-1 rounded-lg text-red-400 hover:text-red-600',
|
|
'hover:bg-red-100 transition-colors'
|
|
)}
|
|
aria-label="Dismiss error"
|
|
>
|
|
<X className="w-4 h-4" />
|
|
</button>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// =============================================================================
|
|
// Header Component
|
|
// =============================================================================
|
|
|
|
interface HeaderProps {
|
|
status: 'idle' | 'loading' | 'streaming' | 'error';
|
|
}
|
|
|
|
function Header({ status }: HeaderProps) {
|
|
return (
|
|
<header
|
|
className={cn(
|
|
'flex items-center justify-between px-6 py-4',
|
|
'border-b border-gray-200/60',
|
|
'bg-gradient-to-r from-[#F0F4F8] to-[#E8ECF0]',
|
|
'shadow-[inset_0_-2px_4px_rgba(0,0,0,0.02)]'
|
|
)}
|
|
>
|
|
<div className="flex items-center gap-3">
|
|
<div
|
|
className={cn(
|
|
'w-10 h-10 rounded-xl flex items-center justify-center',
|
|
'bg-gradient-to-br from-indigo-500 to-purple-600',
|
|
'shadow-[4px_4px_8px_#bfc3cc,-4px_-4px_8px_#ffffff]'
|
|
)}
|
|
>
|
|
<Sparkles className="w-5 h-5 text-white" />
|
|
</div>
|
|
<div>
|
|
<h1 className="text-lg font-bold text-gray-800">Control Center</h1>
|
|
<p className="text-xs text-gray-500">AI-powered assistant</p>
|
|
</div>
|
|
</div>
|
|
<StatusIndicator status={status} />
|
|
</header>
|
|
);
|
|
}
|
|
|
|
|
|
// =============================================================================
|
|
// Main Component
|
|
// =============================================================================
|
|
|
|
export function ChatInterface({ className }: ChatInterfaceProps) {
|
|
const {
|
|
isLoading,
|
|
isStreaming,
|
|
error,
|
|
clearError,
|
|
sendMessage,
|
|
} = useChatContext();
|
|
|
|
// Determine current status
|
|
const getStatus = (): 'idle' | 'loading' | 'streaming' | 'error' => {
|
|
if (error) return 'error';
|
|
if (isStreaming) return 'streaming';
|
|
if (isLoading) return 'loading';
|
|
return 'idle';
|
|
};
|
|
|
|
// Handle message sending from ChatComposer
|
|
const handleSend = useCallback((message: string, provider?: string) => {
|
|
sendMessage(message, provider);
|
|
}, [sendMessage]);
|
|
|
|
return (
|
|
<div
|
|
className={cn(
|
|
'flex flex-col h-full w-full',
|
|
'bg-[#F0F4F8]',
|
|
'rounded-3xl overflow-hidden',
|
|
'shadow-[8px_8px_16px_#b8bcc5,-8px_-8px_16px_#ffffff]',
|
|
'border-2 border-white/50',
|
|
className
|
|
)}
|
|
>
|
|
{/* Header */}
|
|
<Header status={getStatus()} />
|
|
|
|
{/* Error Banner */}
|
|
{error && (
|
|
<ErrorBanner message={error} onDismiss={clearError} />
|
|
)}
|
|
|
|
{/* Message List */}
|
|
<div className="flex-1 min-h-0 overflow-hidden flex flex-col">
|
|
<MessageList />
|
|
</div>
|
|
|
|
{/* Chat Composer */}
|
|
<div
|
|
className={cn(
|
|
'px-4 py-4 border-t border-gray-200/60',
|
|
'bg-gradient-to-r from-[#F0F4F8] to-[#E8ECF0]'
|
|
)}
|
|
>
|
|
<ChatComposer
|
|
onSend={handleSend}
|
|
disabled={isLoading}
|
|
isStreaming={isStreaming}
|
|
/>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export default ChatInterface;
|