f561687307
- 新增 CodeBlock 组件,使用 Shiki 语法高亮 - 新增 Markdown 组件,支持 GFM 语法 - AI 消息自动渲染 Markdown,用户消息保持原样 - 代码块支持一键复制和语言标签显示
135 lines
4.1 KiB
TypeScript
135 lines
4.1 KiB
TypeScript
/**
|
|
* Chat Message Component
|
|
*/
|
|
|
|
import { User, Bot, Copy, Check } from 'lucide-react';
|
|
import { motion } from 'framer-motion';
|
|
import { useState } from 'react';
|
|
import { cn } from '../utils/cn';
|
|
import { fadeInUp, smoothTransition } from '../utils/animations';
|
|
import { Markdown } from './Markdown';
|
|
import type { Message } from '../api/client.js';
|
|
|
|
interface ChatMessageProps {
|
|
message: Message;
|
|
}
|
|
|
|
export function ChatMessage({ message }: ChatMessageProps) {
|
|
const isUser = message.role === 'user';
|
|
const [copied, setCopied] = useState(false);
|
|
|
|
const handleCopy = async () => {
|
|
await navigator.clipboard.writeText(message.content);
|
|
setCopied(true);
|
|
setTimeout(() => setCopied(false), 2000);
|
|
};
|
|
|
|
return (
|
|
<motion.div
|
|
variants={fadeInUp}
|
|
initial="initial"
|
|
animate="animate"
|
|
exit="exit"
|
|
transition={smoothTransition}
|
|
className={cn(
|
|
'group flex gap-4 p-4 rounded-lg',
|
|
isUser ? 'bg-gray-800' : 'bg-gray-800/50'
|
|
)}
|
|
>
|
|
<div
|
|
className={cn(
|
|
'w-8 h-8 rounded-full flex items-center justify-center flex-shrink-0',
|
|
isUser ? 'bg-primary-600' : 'bg-green-600'
|
|
)}
|
|
>
|
|
{isUser ? <User size={18} /> : <Bot size={18} />}
|
|
</div>
|
|
<div className="flex-1 min-w-0 overflow-hidden">
|
|
<div className="flex items-center justify-between mb-1">
|
|
<span className="text-sm text-gray-400">
|
|
{isUser ? 'You' : 'AI Assistant'}
|
|
</span>
|
|
<button
|
|
onClick={handleCopy}
|
|
className="opacity-0 group-hover:opacity-100 p-1 rounded text-gray-500 hover:text-gray-300 hover:bg-gray-700 transition-all"
|
|
title="Copy message"
|
|
>
|
|
{copied ? <Check size={14} className="text-green-500" /> : <Copy size={14} />}
|
|
</button>
|
|
</div>
|
|
<div className="message-content text-gray-200">
|
|
{isUser ? (
|
|
// 用户消息:保持原样显示
|
|
<div className="whitespace-pre-wrap break-words">{message.content}</div>
|
|
) : (
|
|
// AI 消息:Markdown 渲染
|
|
<Markdown content={message.content} />
|
|
)}
|
|
</div>
|
|
</div>
|
|
</motion.div>
|
|
);
|
|
}
|
|
|
|
interface StreamingMessageProps {
|
|
content: string;
|
|
}
|
|
|
|
export function StreamingMessage({ content }: StreamingMessageProps) {
|
|
return (
|
|
<motion.div
|
|
initial={{ opacity: 0, y: 10 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
transition={smoothTransition}
|
|
className="flex gap-4 p-4 rounded-lg bg-gray-800/50"
|
|
>
|
|
<div className="w-8 h-8 rounded-full flex items-center justify-center flex-shrink-0 bg-green-600">
|
|
<Bot size={18} />
|
|
</div>
|
|
<div className="flex-1 min-w-0 overflow-hidden">
|
|
<div className="text-sm text-gray-400 mb-1">AI Assistant</div>
|
|
<div className="message-content text-gray-200">
|
|
<Markdown content={content} />
|
|
<motion.span
|
|
animate={{ opacity: [1, 0] }}
|
|
transition={{ duration: 0.8, repeat: Infinity, repeatType: 'reverse' }}
|
|
className="inline-block w-2 h-4 bg-primary-400 ml-1 rounded-sm align-middle"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</motion.div>
|
|
);
|
|
}
|
|
|
|
export function TypingIndicator() {
|
|
return (
|
|
<motion.div
|
|
initial={{ opacity: 0, y: 10 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
transition={smoothTransition}
|
|
className="flex gap-4 p-4 rounded-lg bg-gray-800/50"
|
|
>
|
|
<div className="w-8 h-8 rounded-full flex items-center justify-center flex-shrink-0 bg-green-600">
|
|
<Bot size={18} />
|
|
</div>
|
|
<div className="flex-1">
|
|
<div className="text-sm text-gray-400 mb-1">AI Assistant</div>
|
|
<div className="flex items-center gap-1 h-6">
|
|
{[0, 1, 2].map((i) => (
|
|
<motion.span
|
|
key={i}
|
|
animate={{ y: [0, -4, 0] }}
|
|
transition={{
|
|
duration: 0.6,
|
|
repeat: Infinity,
|
|
delay: i * 0.15,
|
|
}}
|
|
className="w-2 h-2 rounded-full bg-gray-400"
|
|
/>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</motion.div>
|
|
);
|
|
}
|