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 { 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">
|
||||||
{/* 思考模式开关 */}
|
{/* 思考模式开关 */}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user