- 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>
118 lines
3.7 KiB
TypeScript
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;
|