feat(页面): 聊天页面标题增强功能

- 标题区域新增下拉菜单,支持重命名和删除对话
- 添加内联标题编辑模式
- 优化模型格式转换,区分 name 和 displayName
- 完善键盘快捷键支持(Enter 确认、Escape 取消)
This commit is contained in:
gaoziman 2025-12-20 01:05:11 +08:00
parent 3112bc1f42
commit 2a4ede5726

View File

@ -2,7 +2,7 @@
import { useState, useRef, useEffect, use } from 'react'; import { useState, useRef, useEffect, use } from 'react';
import { useRouter, useSearchParams } from 'next/navigation'; import { useRouter, useSearchParams } from 'next/navigation';
import { Share2, MoreHorizontal, Loader2, Square, Clock } from 'lucide-react'; import { Share2, MoreHorizontal, Loader2, Square, Clock, ChevronDown, Pencil, Trash2, Check, X } from 'lucide-react';
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';
@ -28,9 +28,17 @@ export default function ChatPage({ params }: PageProps) {
const [isNewChat, setIsNewChat] = useState(false); const [isNewChat, setIsNewChat] = useState(false);
const [initialMessageSent, setInitialMessageSent] = useState(false); const [initialMessageSent, setInitialMessageSent] = useState(false);
// 标题下拉菜单状态
const [titleMenuOpen, setTitleMenuOpen] = useState(false);
const [isEditingTitle, setIsEditingTitle] = useState(false);
const [editingTitle, setEditingTitle] = useState('');
const [isSavingTitle, setIsSavingTitle] = useState(false);
const titleInputRef = useRef<HTMLInputElement>(null);
const titleMenuRef = useRef<HTMLDivElement>(null);
// 获取数据 // 获取数据
const { conversation, loading: conversationLoading, error: conversationError } = useConversation(chatId); const { conversation, loading: conversationLoading, error: conversationError } = useConversation(chatId);
const { createConversation } = useConversations(); const { createConversation, updateConversation, deleteConversation } = useConversations();
const { models, loading: modelsLoading } = useModels(); const { models, loading: modelsLoading } = useModels();
const { tools: availableTools, loading: toolsLoading } = useTools(); const { tools: availableTools, loading: toolsLoading } = useTools();
const { settings } = useSettings(); const { settings } = useSettings();
@ -90,6 +98,80 @@ export default function ChatPage({ params }: PageProps) {
scrollToBottom(); scrollToBottom();
}, [messages]); }, [messages]);
// 聚焦标题输入框
useEffect(() => {
if (isEditingTitle && titleInputRef.current) {
titleInputRef.current.focus();
titleInputRef.current.select();
}
}, [isEditingTitle]);
// 点击外部关闭下拉菜单
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (titleMenuRef.current && !titleMenuRef.current.contains(event.target as Node)) {
setTitleMenuOpen(false);
}
};
if (titleMenuOpen) {
document.addEventListener('mousedown', handleClickOutside);
}
return () => {
document.removeEventListener('mousedown', handleClickOutside);
};
}, [titleMenuOpen]);
// 开始重命名
const handleStartRename = () => {
setIsEditingTitle(true);
setEditingTitle(conversation?.title || '');
setTitleMenuOpen(false);
};
// 提交重命名
const handleSubmitRename = async () => {
if (!editingTitle.trim() || isSavingTitle) return;
try {
setIsSavingTitle(true);
await updateConversation(chatId, { title: editingTitle.trim() });
setIsEditingTitle(false);
setEditingTitle('');
// 刷新页面数据
window.location.reload();
} catch (error) {
console.error('Failed to rename conversation:', error);
} finally {
setIsSavingTitle(false);
}
};
// 取消重命名
const handleCancelRename = () => {
setIsEditingTitle(false);
setEditingTitle('');
};
// 处理键盘事件
const handleTitleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === 'Enter') {
e.preventDefault();
handleSubmitRename();
} else if (e.key === 'Escape') {
handleCancelRename();
}
};
// 删除对话
const handleDeleteConversation = async () => {
if (!confirm('Are you sure you want to delete this conversation?')) return;
try {
await deleteConversation(chatId);
router.push('/');
} catch (error) {
console.error('Failed to delete conversation:', error);
}
};
// 处理初始消息(从首页跳转过来时) // 处理初始消息(从首页跳转过来时)
useEffect(() => { useEffect(() => {
if ( if (
@ -171,7 +253,8 @@ export default function ChatPage({ params }: PageProps) {
// 转换模型格式 // 转换模型格式
const modelOptions = models.map((m) => ({ const modelOptions = models.map((m) => ({
id: m.modelId, id: m.modelId,
name: m.displayName, name: m.modelId,
displayName: m.displayName,
tag: m.supportsThinking ? 'Thinking' : '', tag: m.supportsThinking ? 'Thinking' : '',
})); }));
@ -212,9 +295,83 @@ export default function ChatPage({ params }: PageProps) {
<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="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">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<SidebarToggle onClick={() => setSidebarOpen(!sidebarOpen)} /> <SidebarToggle onClick={() => setSidebarOpen(!sidebarOpen)} />
<h1 className="text-base font-medium text-[var(--color-text-primary)] truncate max-w-[300px]">
{conversation?.title || '新对话'} {/* 标题区域 - 可点击显示下拉菜单 */}
</h1> {isEditingTitle ? (
// 编辑模式
<div className="flex items-center gap-2">
<input
ref={titleInputRef}
type="text"
value={editingTitle}
onChange={(e) => setEditingTitle(e.target.value)}
onKeyDown={handleTitleKeyDown}
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]"
disabled={isSavingTitle}
/>
<button
onMouseDown={(e) => {
e.preventDefault();
handleSubmitRename();
}}
disabled={isSavingTitle || !editingTitle.trim()}
className="p-1 text-green-500 hover:bg-[var(--color-bg-hover)] rounded disabled:opacity-50"
title="Confirm"
>
{isSavingTitle ? (
<Loader2 size={16} className="animate-spin" />
) : (
<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>
)}
</div>
)}
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
{/* 思考模式开关 */} {/* 思考模式开关 */}