style(界面): 优化聊天页面和侧边栏样式

- 聊天页面集成 ChatHeader 组件显示助手信息
- 添加淡入、滑入等动画效果样式
- 优化侧边栏布局和新对话按钮交互
- 统一分类标签的视觉样式
This commit is contained in:
gaoziman 2025-12-20 20:46:51 +08:00
parent a5fcc9edae
commit 5307255844
3 changed files with 159 additions and 110 deletions

View File

@ -6,6 +6,7 @@ import { Share2, MoreHorizontal, Loader2, Square, Clock, ChevronDown, Pencil, Tr
import { Sidebar, SidebarToggle } from '@/components/layout/Sidebar'; import { Sidebar, SidebarToggle } from '@/components/layout/Sidebar';
import { ChatInput } from '@/components/features/ChatInput'; import { ChatInput } from '@/components/features/ChatInput';
import { MessageBubble } from '@/components/features/MessageBubble'; import { MessageBubble } from '@/components/features/MessageBubble';
import { ChatHeaderInfo } from '@/components/features/ChatHeader';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import { useConversation, useConversations } from '@/hooks/useConversations'; import { useConversation, useConversations } from '@/hooks/useConversations';
import { useStreamChat, type ChatMessage } from '@/hooks/useStreamChat'; import { useStreamChat, type ChatMessage } from '@/hooks/useStreamChat';
@ -263,6 +264,19 @@ export default function ChatPage({ params }: PageProps) {
setEnableThinking(!enableThinking); setEnableThinking(!enableThinking);
}; };
// 切换模型(持久化到数据库)
const handleModelChange = async (modelId: string) => {
if (!conversation || modelId === selectedModelId) return;
try {
await updateConversation(chatId, { model: modelId });
setSelectedModelId(modelId);
} catch (error) {
console.error('Failed to change model:', error);
throw error;
}
};
// 转换模型格式 // 转换模型格式
const modelOptions = models.map((m) => ({ const modelOptions = models.map((m) => ({
id: m.modelId, id: m.modelId,
@ -305,89 +319,91 @@ export default function ChatPage({ params }: PageProps) {
)} )}
> >
{/* 固定顶部 Header */} {/* 固定顶部 Header */}
<header className="h-[var(--header-height)] px-4 flex items-center justify-between border-b border-[var(--color-border)] bg-[var(--color-bg-primary)] sticky top-0 z-10"> <header className="px-4 py-2 flex flex-col gap-1 border-b border-[var(--color-border)] bg-[var(--color-bg-primary)] sticky top-0 z-10">
<div className="flex items-center gap-3"> {/* 第一行:标题和操作按钮 */}
<SidebarToggle onClick={() => setSidebarOpen(!sidebarOpen)} /> <div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<SidebarToggle onClick={() => setSidebarOpen(!sidebarOpen)} />
{/* 标题区域 - 可点击显示下拉菜单 */} {/* 标题区域 - 可点击显示下拉菜单 */}
{isEditingTitle ? ( {isEditingTitle ? (
// 编辑模式 // 编辑模式
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<input <input
ref={titleInputRef} ref={titleInputRef}
type="text" type="text"
value={editingTitle} value={editingTitle}
onChange={(e) => setEditingTitle(e.target.value)} onChange={(e) => setEditingTitle(e.target.value)}
onKeyDown={handleTitleKeyDown} onKeyDown={handleTitleKeyDown}
onBlur={handleCancelRename} onBlur={handleCancelRename}
className="px-2 py-1 text-base font-medium bg-[var(--color-bg-secondary)] border border-[var(--color-border)] rounded text-[var(--color-text-primary)] focus:outline-none focus:border-[var(--color-primary)] max-w-[300px]" className="px-2 py-1 text-base font-medium bg-[var(--color-bg-secondary)] border border-[var(--color-border)] rounded text-[var(--color-text-primary)] focus:outline-none focus:border-[var(--color-primary)] max-w-[300px]"
disabled={isSavingTitle} disabled={isSavingTitle}
/> />
<button <button
onMouseDown={(e) => { onMouseDown={(e) => {
e.preventDefault(); e.preventDefault();
handleSubmitRename(); handleSubmitRename();
}} }}
disabled={isSavingTitle || !editingTitle.trim()} disabled={isSavingTitle || !editingTitle.trim()}
className="p-1 text-green-500 hover:bg-[var(--color-bg-hover)] rounded disabled:opacity-50" className="p-1 text-green-500 hover:bg-[var(--color-bg-hover)] rounded disabled:opacity-50"
title="Confirm" title="Confirm"
> >
{isSavingTitle ? ( {isSavingTitle ? (
<Loader2 size={16} className="animate-spin" /> <Loader2 size={16} className="animate-spin" />
) : ( ) : (
<Check size={16} /> <Check size={16} />
)}
</button>
<button
onMouseDown={(e) => {
e.preventDefault();
handleCancelRename();
}}
disabled={isSavingTitle}
className="p-1 text-[var(--color-text-tertiary)] hover:bg-[var(--color-bg-hover)] rounded disabled:opacity-50"
title="Cancel"
>
<X size={16} />
</button>
</div>
) : (
// 正常模式 - 显示标题和下拉菜单
<div className="relative" ref={titleMenuRef}>
<button
onClick={() => setTitleMenuOpen(!titleMenuOpen)}
className="flex items-center gap-1 text-base font-medium text-[var(--color-text-primary)] hover:bg-[var(--color-bg-hover)] px-2 py-1 rounded-lg transition-colors max-w-[300px]"
>
<span className="truncate">{conversation?.title || 'New Chat'}</span>
<ChevronDown size={16} className={cn(
'flex-shrink-0 transition-transform',
titleMenuOpen && 'rotate-180'
)} />
</button>
{/* 下拉菜单 */}
{titleMenuOpen && (
<div className="absolute left-0 top-full mt-1 bg-[var(--color-bg-primary)] border border-[var(--color-border)] rounded-lg shadow-lg py-1 z-20 min-w-[140px]">
<button
onClick={handleStartRename}
className="w-full px-3 py-2 text-left text-sm text-[var(--color-text-secondary)] hover:bg-[var(--color-bg-hover)] flex items-center gap-2"
>
<Pencil size={14} />
Rename
</button>
<button
onClick={handleDeleteConversation}
className="w-full px-3 py-2 text-left text-sm text-red-500 hover:bg-[var(--color-bg-hover)] flex items-center gap-2"
>
<Trash2 size={14} />
Delete
</button>
</div>
)} )}
</button> </div>
<button )}
onMouseDown={(e) => { </div>
e.preventDefault(); <div className="flex items-center gap-2">
handleCancelRename(); {/* 思考模式开关 */}
}}
disabled={isSavingTitle}
className="p-1 text-[var(--color-text-tertiary)] hover:bg-[var(--color-bg-hover)] rounded disabled:opacity-50"
title="Cancel"
>
<X size={16} />
</button>
</div>
) : (
// 正常模式 - 显示标题和下拉菜单
<div className="relative" ref={titleMenuRef}>
<button
onClick={() => setTitleMenuOpen(!titleMenuOpen)}
className="flex items-center gap-1 text-base font-medium text-[var(--color-text-primary)] hover:bg-[var(--color-bg-hover)] px-2 py-1 rounded-lg transition-colors max-w-[300px]"
>
<span className="truncate">{conversation?.title || 'New Chat'}</span>
<ChevronDown size={16} className={cn(
'flex-shrink-0 transition-transform',
titleMenuOpen && 'rotate-180'
)} />
</button>
{/* 下拉菜单 */}
{titleMenuOpen && (
<div className="absolute left-0 top-full mt-1 bg-[var(--color-bg-primary)] border border-[var(--color-border)] rounded-lg shadow-lg py-1 z-20 min-w-[140px]">
<button
onClick={handleStartRename}
className="w-full px-3 py-2 text-left text-sm text-[var(--color-text-secondary)] hover:bg-[var(--color-bg-hover)] flex items-center gap-2"
>
<Pencil size={14} />
Rename
</button>
<button
onClick={handleDeleteConversation}
className="w-full px-3 py-2 text-left text-sm text-red-500 hover:bg-[var(--color-bg-hover)] flex items-center gap-2"
>
<Trash2 size={14} />
Delete
</button>
</div>
)}
</div>
)}
</div>
<div className="flex items-center gap-2">
{/* 思考模式开关 */}
<button <button
onClick={handleThinkingToggle} onClick={handleThinkingToggle}
className={cn( className={cn(
@ -415,6 +431,17 @@ export default function ChatPage({ params }: PageProps) {
> >
<MoreHorizontal size={18} /> <MoreHorizontal size={18} />
</button> </button>
</div>
</div>
{/* 第二行:助手和模型信息 */}
<div className="pl-12">
<ChatHeaderInfo
assistant={conversation?.assistant || null}
currentModel={selectedModelId}
models={modelOptions}
onModelChange={handleModelChange}
/>
</div> </div>
</header> </header>

View File

@ -258,6 +258,26 @@ body {
animation: scaleInFast 0.15s ease-out; animation: scaleInFast 0.15s ease-out;
} }
.animate-scale-in {
animation: scaleInFast 0.2s ease-out;
}
/* 卡片淡入上浮动画 */
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(8px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.animate-fade-in-up {
animation: fadeInUp 0.3s ease-out both;
}
/* ======================================== /* ========================================
响应式设计 响应式设计
======================================== */ ======================================== */

View File

@ -2,11 +2,11 @@
import Link from 'next/link'; import Link from 'next/link';
import { usePathname, useRouter } from 'next/navigation'; import { usePathname, useRouter } from 'next/navigation';
import { Plus, PanelLeft, Trash2, MoreHorizontal, Loader2, Pencil, Check, X } from 'lucide-react'; import { Plus, PanelLeft, Trash2, MoreHorizontal, Loader2, Pencil, Check, X, Bot } from 'lucide-react';
import { UserMenu } from '@/components/ui/UserMenu'; import { UserMenu } from '@/components/ui/UserMenu';
import { NewChatModal } from '@/components/features/NewChatModal';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import { useConversations } from '@/hooks/useConversations'; import { useConversations } from '@/hooks/useConversations';
import { useSettings } from '@/hooks/useSettings';
import { useAuth } from '@/providers/AuthProvider'; import { useAuth } from '@/providers/AuthProvider';
import type { Conversation } from '@/drizzle/schema'; import type { Conversation } from '@/drizzle/schema';
import { useState, useRef, useEffect } from 'react'; import { useState, useRef, useEffect } from 'react';
@ -19,14 +19,13 @@ interface SidebarProps {
export function Sidebar({ isOpen = true }: SidebarProps) { export function Sidebar({ isOpen = true }: SidebarProps) {
const pathname = usePathname(); const pathname = usePathname();
const router = useRouter(); const router = useRouter();
const { conversations, loading, createConversation, deleteConversation, updateConversation } = useConversations(); const { conversations, loading, deleteConversation, updateConversation } = useConversations();
const { settings } = useSettings();
const { user } = useAuth(); const { user } = useAuth();
const [creatingChat, setCreatingChat] = useState(false);
const [menuOpen, setMenuOpen] = useState<string | null>(null); const [menuOpen, setMenuOpen] = useState<string | null>(null);
const [editingId, setEditingId] = useState<string | null>(null); const [editingId, setEditingId] = useState<string | null>(null);
const [editingTitle, setEditingTitle] = useState(''); const [editingTitle, setEditingTitle] = useState('');
const [isSubmitting, setIsSubmitting] = useState(false); const [isSubmitting, setIsSubmitting] = useState(false);
const [showNewChatModal, setShowNewChatModal] = useState(false);
const inputRef = useRef<HTMLInputElement>(null); const inputRef = useRef<HTMLInputElement>(null);
const menuRef = useRef<HTMLDivElement>(null); const menuRef = useRef<HTMLDivElement>(null);
@ -53,23 +52,9 @@ export function Sidebar({ isOpen = true }: SidebarProps) {
}; };
}, [menuOpen]); }, [menuOpen]);
// 创建新对话 // 创建新对话 - 显示选择助手弹框
const handleNewChat = async () => { const handleNewChat = () => {
if (creatingChat) return; setShowNewChatModal(true);
try {
setCreatingChat(true);
const newConversation = await createConversation({
model: settings?.defaultModel || 'claude-sonnet-4-20250514',
tools: settings?.defaultTools || [],
enableThinking: settings?.enableThinking || false,
});
router.push(`/chat/${newConversation.conversationId}`);
} catch (error) {
console.error('Failed to create conversation:', error);
} finally {
setCreatingChat(false);
}
}; };
// 删除对话 // 删除对话
@ -150,18 +135,29 @@ export function Sidebar({ isOpen = true }: SidebarProps) {
<div className="px-4 py-2"> <div className="px-4 py-2">
<button <button
onClick={handleNewChat} onClick={handleNewChat}
disabled={creatingChat} className="flex items-center gap-2 text-[var(--color-primary)] text-sm font-medium py-2 hover:opacity-80 transition-opacity"
className="flex items-center gap-2 text-[var(--color-primary)] text-sm font-medium py-2 hover:opacity-80 transition-opacity disabled:opacity-50"
> >
{creatingChat ? ( <Plus size={18} />
<Loader2 size={18} className="animate-spin" />
) : (
<Plus size={18} />
)}
<span>New chat</span> <span>New chat</span>
</button> </button>
</div> </div>
{/* 助手库入口 */}
<div className="px-4 pb-2">
<Link
href="/assistants"
className={cn(
'flex items-center gap-2 text-sm font-medium py-2 transition-colors',
pathname === '/assistants'
? 'text-[var(--color-primary)]'
: 'text-[var(--color-text-secondary)] hover:text-[var(--color-primary)]'
)}
>
<Bot size={18} />
<span></span>
</Link>
</div>
{/* 聊天列表 */} {/* 聊天列表 */}
<nav className="flex-1 overflow-y-auto px-2 flex flex-col gap-1"> <nav className="flex-1 overflow-y-auto px-2 flex flex-col gap-1">
{loading ? ( {loading ? (
@ -301,6 +297,12 @@ export function Sidebar({ isOpen = true }: SidebarProps) {
onClick={() => setMenuOpen(null)} onClick={() => setMenuOpen(null)}
/> />
)} )}
{/* 新建对话弹框 */}
<NewChatModal
isOpen={showNewChatModal}
onClose={() => setShowNewChatModal(false)}
/>
</> </>
); );
} }