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:
@@ -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} />}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user