cre-sync/components/control-center/AIMessageBubble.tsx
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

118 lines
3.7 KiB
TypeScript

'use client';
import React from 'react';
import { Bot } from 'lucide-react';
import type { ControlCenterMessage } from '@/types/control-center';
import { cn } from '@/lib/utils';
import { ToolCallCard } from './ToolCallCard';
import { ToolResultCard } from './ToolResultCard';
interface AIMessageBubbleProps {
message: ControlCenterMessage;
isStreaming?: boolean;
}
export const AIMessageBubble: React.FC<AIMessageBubbleProps> = ({
message,
isStreaming = false,
}) => {
const hasToolCalls = message.toolCalls && message.toolCalls.length > 0;
const hasToolResults = message.toolResults && message.toolResults.length > 0;
const hasContent = message.content && message.content.trim().length > 0;
return (
<div className="flex gap-3 max-w-[85%]">
{/* AI Avatar */}
<div
className={cn(
'shrink-0 w-9 h-9 rounded-xl flex items-center justify-center',
'bg-gradient-to-br from-indigo-500 to-purple-600 text-white',
'shadow-[3px_3px_6px_#c5c9d1,-3px_-3px_6px_#ffffff]'
)}
>
<Bot size={18} />
</div>
{/* Message Content */}
<div className="flex-1 space-y-3">
{/* Text Content Bubble */}
{(hasContent || isStreaming) && (
<div
className={cn(
'bg-[#F0F4F8] rounded-2xl rounded-tl-md p-4',
'shadow-[6px_6px_12px_#bfc3cc,-6px_-6px_12px_#ffffff]',
'border-2 border-transparent transition-all duration-300'
)}
>
{/* Message Text */}
<div className="text-gray-800 text-sm leading-relaxed whitespace-pre-wrap">
{message.content}
{isStreaming && (
<span className="inline-flex ml-1">
<span className="animate-pulse">|</span>
</span>
)}
</div>
{/* Streaming "Thinking" Animation */}
{isStreaming && !hasContent && (
<div className="flex items-center gap-2 text-gray-500">
<div className="flex 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>
<span className="text-xs text-gray-400">Thinking...</span>
</div>
)}
</div>
)}
{/* Tool Calls */}
{hasToolCalls && (
<div className="space-y-2">
{message.toolCalls!.map((toolCall) => (
<ToolCallCard
key={toolCall.id}
toolCall={toolCall}
isExecuting={isStreaming}
/>
))}
</div>
)}
{/* Tool Results */}
{hasToolResults && (
<div className="space-y-2">
{message.toolResults!.map((toolResult) => (
<ToolResultCard
key={toolResult.toolCallId}
toolResult={toolResult}
/>
))}
</div>
)}
{/* Timestamp */}
<div className="text-xs text-gray-400 mt-1 px-1">
{new Date(message.createdAt).toLocaleTimeString([], {
hour: '2-digit',
minute: '2-digit',
})}
</div>
</div>
</div>
);
};
export default AIMessageBubble;