feat(ui): 集成 shadcn/ui 原语、Framer Motion 动画和 Sonner Toast
- 添加 shadcn/ui 风格原语组件 (Button, Input, Dialog, Select, Slider, Switch, Tooltip) - 集成 Framer Motion 动画库,添加动画预设 - 集成 Sonner Toast 通知系统 - 改造 ChatMessage 添加淡入动画和复制按钮 - 改造 Sidebar 添加动画、空状态引导和骨架屏 - 改造 ConfigPanel 使用新原语组件 - 优化 Chat 页面空状态和连接状态指示器 - 添加 tailwindcss-animate 插件
This commit is contained in:
@@ -2,8 +2,11 @@
|
||||
* Chat Message Component
|
||||
*/
|
||||
|
||||
import { User, Bot } from 'lucide-react';
|
||||
import clsx from 'clsx';
|
||||
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 type { Message } from '../api/client.js';
|
||||
|
||||
interface ChatMessageProps {
|
||||
@@ -12,16 +15,28 @@ interface ChatMessageProps {
|
||||
|
||||
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 (
|
||||
<div
|
||||
className={clsx(
|
||||
'flex gap-4 p-4 rounded-lg',
|
||||
isUser ? 'bg-gray-800' : 'bg-gray-850'
|
||||
<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={clsx(
|
||||
className={cn(
|
||||
'w-8 h-8 rounded-full flex items-center justify-center flex-shrink-0',
|
||||
isUser ? 'bg-primary-600' : 'bg-green-600'
|
||||
)}
|
||||
@@ -29,14 +44,23 @@ export function ChatMessage({ message }: ChatMessageProps) {
|
||||
{isUser ? <User size={18} /> : <Bot size={18} />}
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="text-sm text-gray-400 mb-1">
|
||||
{isUser ? 'You' : 'AI Assistant'}
|
||||
<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 whitespace-pre-wrap break-words">
|
||||
{message.content}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -46,7 +70,12 @@ interface StreamingMessageProps {
|
||||
|
||||
export function StreamingMessage({ content }: StreamingMessageProps) {
|
||||
return (
|
||||
<div className="flex gap-4 p-4 rounded-lg bg-gray-850">
|
||||
<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>
|
||||
@@ -54,27 +83,45 @@ export function StreamingMessage({ content }: StreamingMessageProps) {
|
||||
<div className="text-sm text-gray-400 mb-1">AI Assistant</div>
|
||||
<div className="message-content whitespace-pre-wrap break-words">
|
||||
{content}
|
||||
<span className="inline-block w-2 h-4 bg-gray-400 animate-pulse ml-1" />
|
||||
<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"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
|
||||
export function TypingIndicator() {
|
||||
return (
|
||||
<div className="flex gap-4 p-4 rounded-lg bg-gray-850">
|
||||
<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="typing-indicator">
|
||||
<span />
|
||||
<span />
|
||||
<span />
|
||||
<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>
|
||||
</div>
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user