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:
2025-12-12 17:20:41 +08:00
parent 67c6918b28
commit cbbe9c7af1
26 changed files with 2272 additions and 514 deletions
+66 -19
View File
@@ -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>
);
}