/** 对话历史页:按场景分组列表、查看详情、保密模式密码验证、删除会话 */ import { useEffect, useState, useCallback } from 'react' import { toast } from 'sonner' import { History, Lock, Trash2, Loader2, MessageSquare, Shield, ChevronRight, ArrowLeft, BookOpen, Sparkles, } from 'lucide-react' import { PageLayout } from '@/components/common/PageLayout' import { EmptyState } from '@/components/common/EmptyState' import { LoadingState } from '@/components/common/LoadingState' import { ErrorState } from '@/components/common/ErrorState' import { Markdown } from '@/components/common/Markdown' import { Card, CardContent } from '@/components/ui/card' import { Button } from '@/components/ui/button' import { Input } from '@/components/ui/input' import { Label } from '@/components/ui/label' import { Badge } from '@/components/ui/badge' import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from '@/components/ui/dialog' import { api } from '@/lib/api' import { cn, formatTime } from '@/lib/utils' import type { HistorySession, HistoryMessage, SessionMode } from '@/types' /** 模式标签配置 */ const MODE_CONFIG: Record = { normal: { label: '普通模式', icon: MessageSquare, color: 'bg-blue-100 text-blue-700' }, confidential: { label: '保密模式', icon: Lock, color: 'bg-amber-100 text-amber-700' }, ephemeral: { label: '阅后即焚', icon: Shield, color: 'bg-rose-100 text-rose-700' }, } /** 风险等级颜色 */ const RISK_COLORS: Record = { green: 'bg-green-100 text-green-700', amber: 'bg-yellow-100 text-yellow-700', orange: 'bg-orange-100 text-orange-700', red: 'bg-red-100 text-red-700', } /** 知识库文档类型标签映射 */ const DOC_TYPE_LABELS: Record = { law: '法规', case: '案例', guide: '指南', industry: '行业', template: '模板', } /** 场景中文映射 */ const SCENE_LABELS: Record = { legal: '私人法务', decision: '经营决策', life: '生活助理', emotion: '情绪中心', } export function ChatHistory() { const [sessions, setSessions] = useState([]) const [loading, setLoading] = useState(true) const [error, setError] = useState(null) const [modeFilter, setModeFilter] = useState<'all' | 'normal' | 'confidential'>('all') // 当前查看的会话 const [currentSession, setCurrentSession] = useState(null) const [messages, setMessages] = useState([]) const [msgLoading, setMsgLoading] = useState(false) const [msgError, setMsgError] = useState(null) const [needVerify, setNeedVerify] = useState(false) // 保密模式密码验证 const [verifyPassword, setVerifyPassword] = useState('') const [verifying, setVerifying] = useState(false) // 删除确认 const [deleteTarget, setDeleteTarget] = useState(null) const [deleting, setDeleting] = useState(false) const loadSessions = useCallback(async () => { setLoading(true) setError(null) try { const mode = modeFilter === 'all' ? undefined : modeFilter const res = await api.listHistorySessions(mode) setSessions(res.results) } catch (err) { setError(err instanceof Error ? err.message : '加载失败') } finally { setLoading(false) } }, [modeFilter]) useEffect(() => { loadSessions() }, [loadSessions]) /** 查看会话历史 */ const handleView = useCallback(async (session: HistorySession) => { setCurrentSession(session) setMessages([]) setMsgError(null) setNeedVerify(false) setVerifyPassword('') setMsgLoading(true) try { const res = await api.getHistoryMessages(session.session_id) if (res.need_verify) { setNeedVerify(true) } else { setMessages(res.messages || []) } } catch (err) { setMsgError(err instanceof Error ? err.message : '加载失败') } finally { setMsgLoading(false) } }, []) /** 保密模式密码验证 */ const handleVerify = useCallback(async () => { if (!currentSession || !verifyPassword) return setVerifying(true) setMsgError(null) try { const res = await api.verifyHistory(currentSession.session_id, verifyPassword) setMessages(res.messages) setNeedVerify(false) toast.success('验证通过,已解密历史消息') } catch (err) { setMsgError(err instanceof Error ? err.message : '验证失败') } finally { setVerifying(false) } }, [currentSession, verifyPassword]) /** 删除会话历史 */ const handleDelete = useCallback(async () => { if (!deleteTarget) return setDeleting(true) try { await api.deleteHistory(deleteTarget.session_id) toast.success('已删除该会话历史') setDeleteTarget(null) // 如果删除的是当前查看的会话,返回列表 if (currentSession?.session_id === deleteTarget.session_id) { setCurrentSession(null) } loadSessions() } catch (err) { toast.error(err instanceof Error ? err.message : '删除失败') } finally { setDeleting(false) } }, [deleteTarget, currentSession, loadSessions]) /** 返回列表 */ const handleBack = useCallback(() => { setCurrentSession(null) setMessages([]) setNeedVerify(false) setVerifyPassword('') }, []) // ===== 会话详情视图 ===== if (currentSession) { const modeCfg = MODE_CONFIG[currentSession.mode] return ( } >

{currentSession.title || '未命名对话'}

{modeCfg.label} {currentSession.scene && ( {SCENE_LABELS[currentSession.scene] || currentSession.scene} )} {formatTime(currentSession.first_message_at)} ~ {formatTime(currentSession.last_message_at)} {' · '}{currentSession.message_count} 条消息
{/* 保密模式密码验证 */} {needVerify ? (

保密模式历史

该会话为加密存储,请输入登录密码验证后查看

setVerifyPassword(e.target.value)} onKeyDown={(e) => e.key === 'Enter' && handleVerify()} placeholder="请输入登录密码" className="mt-1" />
{msgError &&

{msgError}

}
) : msgLoading ? ( ) : msgError ? ( handleView(currentSession)} /> ) : messages.length === 0 ? ( ) : (
{messages.map((msg) => ( ))}
)}
{/* 删除确认弹窗 */} !o && setDeleteTarget(null)} onConfirm={handleDelete} deleting={deleting} />
) } // ===== 会话列表视图 ===== return ( {(['all', 'normal', 'confidential'] as const).map((m) => ( ))} } > {loading ? ( ) : error ? ( ) : sessions.length === 0 ? ( ) : (
{/* 按场景分组,每组内按时间倒序 */} {(() => { // 定义场景顺序 const SCENE_ORDER = ['legal', 'decision', 'life', 'emotion'] // 按 scene 分组(无 scene 归为"其他") const groups = new Map() for (const s of sessions) { const key = s.scene || 'other' if (!groups.has(key)) groups.set(key, []) groups.get(key)!.push(s) } // 每组内按 last_message_at 倒序 for (const items of groups.values()) { items.sort((a, b) => { const ta = a.last_message_at ? new Date(a.last_message_at).getTime() : 0 const tb = b.last_message_at ? new Date(b.last_message_at).getTime() : 0 return tb - ta }) } // 渲染:按 SCENE_ORDER 顺序,最后是 other const orderedKeys = [...SCENE_ORDER.filter(k => groups.has(k)), ...['other']].filter(k => groups.has(k)) return orderedKeys.map((key) => (

{SCENE_LABELS[key] || '其他'} {groups.get(key)!.length}

{groups.get(key)!.map((session) => { const modeCfg = MODE_CONFIG[session.mode] return ( handleView(session)} >

{session.title || '未命名对话'}

{modeCfg.label} {session.message_count} 条 · {formatTime(session.last_message_at)}
) })}
)) })()}
)} !o && setDeleteTarget(null)} onConfirm={handleDelete} deleting={deleting} />
) } /** 单条消息气泡 */ function MessageBubble({ message }: { message: HistoryMessage }) { const isUser = message.role === 'user' const riskLevel = message.risk?.level return (
{/* 消息正文:用户消息纯文本,AI 消息 Markdown 渲染 */} {isUser ? (

{message.content}

) : ( )} {/* AI 回复的风险卡信息 */} {!isUser && riskLevel && (
{riskLevel === 'green' ? '低风险' : riskLevel === 'amber' ? '需补证' : riskLevel === 'orange' ? '建议复核' : '高风险'} {message.risk?.escalation && ( 建议升级 )} {message.meta?.inference_location && ( {message.meta.inference_location === 'cloud' ? '云端' : '本地'} )}
{message.risk?.actions && message.risk.actions.length > 0 && (
    {message.risk.actions.map((a, i) => (
  • • {a}
  • ))}
)}
)} {/* 参考来源:知识库引用 tag 或 AI 提供 tag */} {!isUser && (
参考来源
{message.meta?.rag_context && message.meta.rag_context.length > 0 ? (
{message.meta.rag_context.map((doc, i) => (
{doc.title} {doc.doc_type && ( {DOC_TYPE_LABELS[doc.doc_type] || doc.doc_type} )} {doc.region && ( {doc.region} )}
))}
) : (
AI 提供
)}
)} {/* 时间戳 */}

{formatTime(message.created_at)}

) } /** 删除确认弹窗 */ function DeleteDialog({ open, onOpenChange, onConfirm, deleting, }: { open: boolean onOpenChange: (open: boolean) => void onConfirm: () => void deleting: boolean }) { return ( 删除对话历史 确定要删除该会话的所有历史消息吗?此操作不可恢复。 {open && ' 保密模式的加密数据将一并清除。'} ) }