feat(web): 添加全局 Toast 通知系统和资产预览导航功能

- 新增 ToastProvider 和 useToast hook,支持全局成功/错误/信息提示
- 资产预览增加左右导航按钮、键盘快捷键(方向键)和计数器显示
- 资产库增加图片/视频类型筛选标签页及计数
- 对话列表增加最近对话展示、搜索优化和删除确认
- 消息增加删除确认对话框
- 优化聊天自动滚动行为,仅在用户未手动滚动时跟随新内容
- 新增删除消息 API 端点
- 优化消息历史清理逻辑,过滤错误消息和孤儿 tool 消息
- 添加自定义滚动条样式
- 优化账户参考图显示逻辑,支持本地文件显示
- 修复对话创建流程,直接导航到新创建的对话
This commit is contained in:
2026-05-08 00:23:36 +08:00
parent 088bdb9a8e
commit 66d170066a
23 changed files with 801 additions and 374 deletions

View File

@@ -1,7 +1,8 @@
import { useState, useCallback } from 'react';
import { cn } from '@/lib/utils';
import { renderMarkdown } from '@/lib/markdown';
import { Bot, User, Copy, Trash2, ChevronDown, ChevronRight, RefreshCw, ArrowRight, Quote, Check, Brain } from 'lucide-react';
import { Bot, User, Copy, Trash2, ChevronDown, ChevronRight, RefreshCw, ArrowRight, Quote, Brain } from 'lucide-react';
import { useToast } from '@/hooks/useToast';
import type { Message } from '@/types';
type MessageState = 'streaming' | 'thinking' | 'done';
@@ -26,16 +27,20 @@ function getState(msg: Message, isLast: boolean, isThinking: boolean): MessageSt
export function ChatMessage({ message, isLast, isThinking, onRegenerate, onContinue, onQuote, onDelete }: Props) {
const isUser = message.role === 'user';
const isTool = message.role === 'tool';
const [copied, setCopied] = useState(false);
const [reasoningOpen, setReasoningOpen] = useState(false);
const { toast } = useToast();
const state = getState(message, !!isLast, !!isThinking);
const handleCopy = useCallback(() => {
navigator.clipboard.writeText(message.content).then(() => {
setCopied(true);
setTimeout(() => setCopied(false), 2000);
toast('已复制到剪贴板', 'success');
});
}, [message.content]);
}, [message.content, toast]);
const handleDelete = useCallback(() => {
if (!confirm('确定删除这条消息?')) return;
onDelete?.(message.id);
}, [message.id, onDelete]);
// Tool messages
if (isTool) {
@@ -125,13 +130,13 @@ export function ChatMessage({ message, isLast, isThinking, onRegenerate, onConti
<div className="flex gap-0.5 mt-1 opacity-0 group-hover/msg:opacity-100 transition-opacity px-1">
<button onClick={handleCopy}
className="flex items-center gap-1 px-1.5 py-0.5 rounded text-[10px] text-zinc-400 hover:text-zinc-600 hover:bg-zinc-100 transition-colors">
{copied ? <><Check size={10} /></> : <><Copy size={10} /></>}
<Copy size={10} />
</button>
<button onClick={() => onQuote?.(message.content)}
className="flex items-center gap-1 px-1.5 py-0.5 rounded text-[10px] text-zinc-400 hover:text-zinc-600 hover:bg-zinc-100 transition-colors">
<Quote size={10} />
</button>
<button onClick={() => onDelete?.(message.id)}
<button onClick={handleDelete}
className="flex items-center gap-1 px-1.5 py-0.5 rounded text-[10px] text-zinc-400 hover:text-red-500 hover:bg-red-50 transition-colors">
<Trash2 size={10} />
</button>