Initial commit: Portal Brief Inteligente CDC
This commit is contained in:
@@ -0,0 +1,187 @@
|
||||
import React from 'react';
|
||||
import { motion } from 'motion/react';
|
||||
import { format } from 'date-fns';
|
||||
import { AlertCircle, CheckCheck, FileText, Music, ShieldCheck } from 'lucide-react';
|
||||
import { Message, Attachment } from '../types';
|
||||
import { cn } from '../lib/utils';
|
||||
import ReactMarkdown from 'react-markdown';
|
||||
|
||||
interface MessageBubbleProps {
|
||||
message: Message;
|
||||
}
|
||||
|
||||
const AttachmentPreview = ({ attachment }: { attachment: Attachment }) => {
|
||||
if (attachment.type === 'image') {
|
||||
return (
|
||||
<div className="group relative max-w-[320px] overflow-hidden rounded-2xl border border-white/10 bg-black/20">
|
||||
<img
|
||||
src={attachment.url}
|
||||
alt={attachment.file.name}
|
||||
className="h-full w-full object-cover transition duration-300 group-hover:scale-[1.02]"
|
||||
/>
|
||||
<div className="absolute inset-x-0 bottom-0 bg-gradient-to-t from-black/80 to-transparent px-3 py-2 text-[11px] font-medium text-white/90">
|
||||
<div className="truncate">{attachment.file.name}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const Icon = attachment.type === 'audio' ? Music : FileText;
|
||||
|
||||
return (
|
||||
<div className="flex w-full max-w-[360px] items-center gap-3 rounded-2xl border border-white/10 bg-white/5 px-3 py-3">
|
||||
<div className="flex h-11 w-11 items-center justify-center rounded-xl bg-black/25">
|
||||
<Icon className="h-4 w-4 text-indigo-200" />
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="truncate text-sm font-medium text-white/90">{attachment.file.name}</p>
|
||||
<p className="text-[11px] text-zinc-400">{(attachment.file.size / 1024).toFixed(1)} KB</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const MessageBubble: React.FC<MessageBubbleProps> = ({ message }) => {
|
||||
const isUser = message.sender === 'user';
|
||||
const [isPlaying, setIsPlaying] = React.useState(false);
|
||||
const audioRef = React.useRef<HTMLAudioElement | null>(null);
|
||||
|
||||
const togglePlay = () => {
|
||||
if (audioRef.current) {
|
||||
if (isPlaying) {
|
||||
audioRef.current.pause();
|
||||
} else {
|
||||
audioRef.current.play();
|
||||
}
|
||||
setIsPlaying(!isPlaying);
|
||||
}
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
if (message.audio) {
|
||||
audioRef.current = new Audio(message.audio.url);
|
||||
audioRef.current.onended = () => setIsPlaying(false);
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (audioRef.current) {
|
||||
audioRef.current.pause();
|
||||
audioRef.current = null;
|
||||
}
|
||||
};
|
||||
}, [message.audio]);
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 14, scale: 0.98 }}
|
||||
animate={{ opacity: 1, y: 0, scale: 1 }}
|
||||
transition={{ duration: 0.32, ease: [0.23, 1, 0.32, 1] }}
|
||||
className={cn('mb-5 flex w-full', isUser ? 'justify-end' : 'justify-start')}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
'flex flex-col gap-1',
|
||||
isUser ? 'max-w-3xl items-end' : 'w-full max-w-none items-start'
|
||||
)}
|
||||
>
|
||||
{!isUser && (
|
||||
<div className="mb-1 inline-flex items-center gap-2 rounded-full border border-white/10 bg-white/5 px-3 py-1 text-[11px] font-semibold uppercase tracking-[0.2em] text-zinc-400">
|
||||
<ShieldCheck className="h-3.5 w-3.5 text-indigo-300" />
|
||||
Respuesta del portal
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div
|
||||
className={cn(
|
||||
'relative overflow-hidden border shadow-[0_16px_40px_rgba(0,0,0,0.25)]',
|
||||
isUser
|
||||
? 'max-w-[85%] rounded-[26px] rounded-br-md border-indigo-400/20 bg-[linear-gradient(135deg,rgba(79,70,229,0.96),rgba(37,99,235,0.92))] px-4 py-4 text-white sm:px-5'
|
||||
: 'w-full max-w-none rounded-[28px] rounded-bl-md border-white/10 bg-[linear-gradient(180deg,rgba(255,255,255,0.07),rgba(255,255,255,0.03))] px-5 py-5 text-zinc-100 backdrop-blur-xl sm:px-6 sm:py-6'
|
||||
)}
|
||||
>
|
||||
{!isUser && (
|
||||
<div className="pointer-events-none absolute inset-0 bg-[radial-gradient(circle_at_top_right,rgba(129,140,248,0.14),transparent_26%)]" />
|
||||
)}
|
||||
|
||||
<div className="relative">
|
||||
{message.attachments && message.attachments.length > 0 && (
|
||||
<div className="mb-4 flex flex-wrap gap-3">
|
||||
{message.attachments.map((att) => (
|
||||
<AttachmentPreview key={att.id} attachment={att} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{message.audio && (
|
||||
<div className="mb-4 flex items-center gap-3 rounded-2xl bg-black/20 p-3 pr-4">
|
||||
<button
|
||||
onClick={togglePlay}
|
||||
className="flex h-10 w-10 items-center justify-center rounded-full bg-white/15 transition hover:bg-white/25"
|
||||
>
|
||||
{isPlaying ? (
|
||||
<div className="flex h-3 w-3 gap-1">
|
||||
<div className="h-full w-1 rounded-sm bg-white" />
|
||||
<div className="h-full w-1 rounded-sm bg-white" />
|
||||
</div>
|
||||
) : (
|
||||
<div className="ml-0.5 h-0 w-0 border-b-[6px] border-l-[9px] border-t-[6px] border-b-transparent border-l-white border-t-transparent" />
|
||||
)}
|
||||
</button>
|
||||
|
||||
<div className="h-1.5 w-28 overflow-hidden rounded-full bg-white/15">
|
||||
<div
|
||||
className={cn(
|
||||
'h-full w-1/3 rounded-full bg-white/65',
|
||||
isPlaying && 'animate-pulse'
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<span className="text-xs font-mono opacity-80">
|
||||
0:{message.audio.duration.toString().padStart(2, '0')}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{message.text && (
|
||||
<div
|
||||
className={cn(
|
||||
'break-words text-[15px] leading-7 sm:text-base',
|
||||
isUser ? 'whitespace-pre-wrap' : ''
|
||||
)}
|
||||
>
|
||||
{isUser ? (
|
||||
message.text
|
||||
) : (
|
||||
<div className="prose prose-invert max-w-none prose-p:my-3 prose-p:leading-8 prose-headings:mb-3 prose-headings:mt-1 prose-headings:text-white prose-strong:text-white prose-li:my-1 prose-ul:my-3 prose-ol:my-3 prose-pre:overflow-x-auto prose-pre:rounded-2xl prose-pre:border prose-pre:border-white/10 prose-pre:bg-black/35 prose-pre:p-4 prose-pre:text-[13px] prose-code:text-indigo-200">
|
||||
<ReactMarkdown>{message.text}</ReactMarkdown>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-1 flex items-center gap-1.5 px-1">
|
||||
<span className="text-[11px] font-medium text-zinc-500">
|
||||
{format(message.timestamp, 'HH:mm')}
|
||||
</span>
|
||||
|
||||
{isUser && (
|
||||
<span className="text-zinc-500">
|
||||
{message.status === 'sending' && (
|
||||
<AlertCircle className="h-3.5 w-3.5 animate-pulse text-zinc-400" />
|
||||
)}
|
||||
{message.status === 'sent' && (
|
||||
<CheckCheck className="h-3.5 w-3.5 text-indigo-400" />
|
||||
)}
|
||||
{message.status === 'error' && (
|
||||
<AlertCircle className="h-3.5 w-3.5 text-red-500" />
|
||||
)}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user