style(界面): 优化聊天页面和侧边栏样式
- 聊天页面集成 ChatHeader 组件显示助手信息 - 添加淡入、滑入等动画效果样式 - 优化侧边栏布局和新对话按钮交互 - 统一分类标签的视觉样式
This commit is contained in:
parent
a5fcc9edae
commit
5307255844
@ -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>
|
||||||
|
|
||||||
|
|||||||
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
/* ========================================
|
/* ========================================
|
||||||
响应式设计
|
响应式设计
|
||||||
======================================== */
|
======================================== */
|
||||||
|
|||||||
@ -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)}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user