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
+79 -51
View File
@@ -3,7 +3,8 @@
*/
import { useEffect, useRef } from 'react';
import { Wifi, WifiOff } from 'lucide-react';
import { WifiOff, MessageSquare, Settings, FolderOpen } from 'lucide-react';
import { motion, AnimatePresence } from 'framer-motion';
import {
useChat,
ChatMessage,
@@ -52,6 +53,66 @@ export function ChatPage({
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
}, [messages, streamingContent]);
// 空状态组件
const EmptyState = () => (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.4 }}
className="text-center py-20"
>
<div className="w-16 h-16 mx-auto mb-6 rounded-full bg-gradient-to-br from-primary-500/20 to-primary-600/20 flex items-center justify-center">
<MessageSquare size={32} className="text-primary-400" />
</div>
<h2 className="text-2xl font-semibold mb-2 bg-gradient-to-r from-white to-gray-400 bg-clip-text text-transparent">
Start a conversation
</h2>
<p className="text-gray-400 mb-6 max-w-md mx-auto">
Ask me anything about coding, debugging, or software development.
</p>
<div className="flex flex-wrap justify-center gap-2">
{['Help me debug this code', 'Explain this function', 'Write a test'].map((suggestion) => (
<motion.button
key={suggestion}
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
onClick={() => sendMessage(suggestion)}
className="px-3 py-1.5 bg-gray-800 hover:bg-gray-700 rounded-full text-sm text-gray-300 transition-colors"
>
"{suggestion}"
</motion.button>
))}
</div>
</motion.div>
);
// 连接状态指示器
const ConnectionStatus = () => (
<div className="flex items-center gap-1.5 text-sm">
{isConnected ? (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
className="flex items-center gap-1.5"
>
<span className="relative flex h-2.5 w-2.5">
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-green-400 opacity-75" />
<span className="relative inline-flex rounded-full h-2.5 w-2.5 bg-green-500" />
</span>
<span className="text-green-400 hidden sm:inline">Connected</span>
</motion.div>
) : (
<div className="flex items-center gap-1.5 text-red-400">
<WifiOff size={16} />
<span className="hidden sm:inline">Disconnected</span>
</div>
)}
</div>
);
return (
<div className="flex-1 flex flex-col h-screen">
{/* Header */}
@@ -59,50 +120,29 @@ export function ChatPage({
<h1 className="text-lg font-medium">Chat</h1>
<div className="flex items-center gap-3">
{/* 连接状态 */}
<div className="flex items-center gap-1.5 text-sm">
{isConnected ? (
<>
<Wifi size={16} className="text-green-500" />
<span className="text-green-500 hidden sm:inline">Connected</span>
</>
) : (
<>
<WifiOff size={16} className="text-red-500" />
<span className="text-red-500 hidden sm:inline">Disconnected</span>
</>
)}
</div>
<ConnectionStatus />
{/* 工具栏按钮 */}
{(onOpenConfig || onToggleFileBrowser) && (
<div className="flex items-center gap-1.5 border-l border-gray-600 pl-3">
{/* 配置按钮 */}
{onOpenConfig && (
<button
<motion.button
whileHover={{ scale: 1.1 }}
whileTap={{ scale: 0.9 }}
onClick={onOpenConfig}
className="p-1.5 rounded-lg text-gray-400 hover:text-gray-200 hover:bg-gray-700 transition-colors"
title="Settings"
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"
/>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"
/>
</svg>
</button>
<Settings size={20} />
</motion.button>
)}
{/* 文件浏览器按钮 - 仅桌面端显示 */}
{onToggleFileBrowser && (
<button
<motion.button
whileHover={{ scale: 1.1 }}
whileTap={{ scale: 0.9 }}
onClick={onToggleFileBrowser}
className={`hidden md:block p-1.5 rounded-lg transition-colors ${
showFileBrowser
@@ -111,15 +151,8 @@ export function ChatPage({
}`}
title={showFileBrowser ? 'Hide Files' : 'Show Files'}
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z"
/>
</svg>
</button>
<FolderOpen size={20} />
</motion.button>
)}
</div>
)}
@@ -129,18 +162,13 @@ export function ChatPage({
{/* Messages */}
<div className="flex-1 overflow-y-auto p-6">
<div className="max-w-4xl mx-auto space-y-4">
{messages.length === 0 && !isLoading && (
<div className="text-center py-20">
<h2 className="text-2xl font-semibold mb-2">Start a conversation</h2>
<p className="text-gray-400">
Type a message below to begin chatting with your AI assistant.
</p>
</div>
)}
{messages.length === 0 && !isLoading && <EmptyState />}
{messages.map((message) => (
<ChatMessage key={message.id} message={message} />
))}
<AnimatePresence mode="popLayout">
{messages.map((message) => (
<ChatMessage key={message.id} message={message} />
))}
</AnimatePresence>
{streamingContent && <StreamingMessage content={streamingContent} />}