feat(页面): 聊天页面标题增强功能
- 标题区域新增下拉菜单,支持重命名和删除对话 - 添加内联标题编辑模式 - 优化模型格式转换,区分 name 和 displayName - 完善键盘快捷键支持(Enter 确认、Escape 取消)
This commit is contained in:
parent
3112bc1f42
commit
2a4ede5726
@ -2,7 +2,7 @@
|
||||
|
||||
import { useState, useRef, useEffect, use } from 'react';
|
||||
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 { ChatInput } from '@/components/features/ChatInput';
|
||||
import { MessageBubble } from '@/components/features/MessageBubble';
|
||||
@ -28,9 +28,17 @@ export default function ChatPage({ params }: PageProps) {
|
||||
const [isNewChat, setIsNewChat] = 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 { createConversation } = useConversations();
|
||||
const { createConversation, updateConversation, deleteConversation } = useConversations();
|
||||
const { models, loading: modelsLoading } = useModels();
|
||||
const { tools: availableTools, loading: toolsLoading } = useTools();
|
||||
const { settings } = useSettings();
|
||||
@ -90,6 +98,80 @@ export default function ChatPage({ params }: PageProps) {
|
||||
scrollToBottom();
|
||||
}, [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(() => {
|
||||
if (
|
||||
@ -171,7 +253,8 @@ export default function ChatPage({ params }: PageProps) {
|
||||
// 转换模型格式
|
||||
const modelOptions = models.map((m) => ({
|
||||
id: m.modelId,
|
||||
name: m.displayName,
|
||||
name: m.modelId,
|
||||
displayName: m.displayName,
|
||||
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">
|
||||
<div className="flex items-center gap-3">
|
||||
<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 className="flex items-center gap-2">
|
||||
{/* 思考模式开关 */}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user