import { MessageSquare, Search, Edit, Trash2, Eye, Plus, Clock, Hash, ChevronLeft, ChevronRight, ChevronsLeft, ChevronsRight, Info, CheckCircle2, XCircle, Circle, ClipboardCheck } from 'lucide-react' import { useState, useEffect } from 'react' import { cn } from '@/lib/utils' import { Button } from '@/components/ui/button' import { Input } from '@/components/ui/input' import { Label } from '@/components/ui/label' import { ScrollArea } from '@/components/ui/scroll-area' import { useToast } from '@/hooks/use-toast' import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from '@/components/ui/table' import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from '@/components/ui/dialog' import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, } from '@/components/ui/alert-dialog' import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '@/components/ui/select' import { Checkbox } from '@/components/ui/checkbox' import { Switch } from '@/components/ui/switch' import { Alert, AlertDescription } from '@/components/ui/alert' import type { Expression, ExpressionCreateRequest, ExpressionUpdateRequest, ChatInfo } from '@/types/expression' import { getExpressionList, getExpressionDetail, createExpression, updateExpression, deleteExpression, batchDeleteExpressions, getExpressionStats, getChatList, getReviewStats } from '@/lib/expression-api' import { ExpressionReviewer } from '@/components/expression-reviewer' export function ExpressionManagementPage() { const [expressions, setExpressions] = useState([]) const [loading, setLoading] = useState(true) const [total, setTotal] = useState(0) const [page, setPage] = useState(1) const [pageSize, setPageSize] = useState(20) const [search, setSearch] = useState('') const [selectedExpression, setSelectedExpression] = useState(null) const [isDetailDialogOpen, setIsDetailDialogOpen] = useState(false) const [isEditDialogOpen, setIsEditDialogOpen] = useState(false) const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false) const [deleteConfirmExpression, setDeleteConfirmExpression] = useState(null) const [selectedIds, setSelectedIds] = useState>(new Set()) const [isBatchDeleteDialogOpen, setIsBatchDeleteDialogOpen] = useState(false) const [jumpToPage, setJumpToPage] = useState('') const [stats, setStats] = useState({ total: 0, recent_7days: 0, chat_count: 0, top_chats: {} as Record }) const [chatList, setChatList] = useState([]) const [chatNameMap, setChatNameMap] = useState>(new Map()) const [isReviewerOpen, setIsReviewerOpen] = useState(false) const [uncheckedCount, setUncheckedCount] = useState(0) const { toast } = useToast() // 加载表达方式列表 const loadExpressions = async () => { try { setLoading(true) const response = await getExpressionList({ page, page_size: pageSize, search: search || undefined, }) setExpressions(response.data) setTotal(response.total) } catch (error) { toast({ title: '加载失败', description: error instanceof Error ? error.message : '无法加载表达方式', variant: 'destructive', }) } finally { setLoading(false) } } // 加载统计数据 const loadStats = async () => { try { const response = await getExpressionStats() if (response?.data) { setStats(response.data) } } catch (error) { console.error('加载统计数据失败:', error) } } // 加载审核统计 const loadReviewStats = async () => { try { const data = await getReviewStats() setUncheckedCount(data.unchecked) } catch (error) { console.error('加载审核统计失败:', error) } } // 加载聊天列表 const loadChatList = async () => { try { const response = await getChatList() if (response?.data) { setChatList(response.data) // 构建聊天ID到名称的映射 const nameMap = new Map() response.data.forEach((chat) => { nameMap.set(chat.chat_id, chat.chat_name) }) setChatNameMap(nameMap) } } catch (error) { console.error('加载聊天列表失败:', error) } } // 获取聊天名称(支持Unicode字符完整显示) const getChatName = (chatId: string): string => { return chatNameMap.get(chatId) || chatId } // 初始加载 useEffect(() => { loadExpressions() loadReviewStats() loadStats() loadChatList() // eslint-disable-next-line react-hooks/exhaustive-deps }, [page, pageSize, search]) // 查看详情 const handleViewDetail = async (expression: Expression) => { try { const response = await getExpressionDetail(expression.id) setSelectedExpression(response.data) setIsDetailDialogOpen(true) } catch (error) { toast({ title: '加载详情失败', description: error instanceof Error ? error.message : '无法加载表达方式详情', variant: 'destructive', }) } } // 编辑表达方式 const handleEdit = (expression: Expression) => { setSelectedExpression(expression) setIsEditDialogOpen(true) } // 删除表达方式 const handleDelete = async (expression: Expression) => { try { await deleteExpression(expression.id) toast({ title: '删除成功', description: `已删除表达方式: ${expression.situation}`, }) setDeleteConfirmExpression(null) loadExpressions() loadStats() } catch (error) { toast({ title: '删除失败', description: error instanceof Error ? error.message : '无法删除表达方式', variant: 'destructive', }) } } // 切换单个选择 const toggleSelect = (id: number) => { const newSelected = new Set(selectedIds) if (newSelected.has(id)) { newSelected.delete(id) } else { newSelected.add(id) } setSelectedIds(newSelected) } // 全选/取消全选 const toggleSelectAll = () => { if (selectedIds.size === expressions.length && expressions.length > 0) { setSelectedIds(new Set()) } else { setSelectedIds(new Set(expressions.map(e => e.id))) } } // 批量删除 const handleBatchDelete = async () => { try { await batchDeleteExpressions(Array.from(selectedIds)) toast({ title: '批量删除成功', description: `已删除 ${selectedIds.size} 个表达方式`, }) setSelectedIds(new Set()) setIsBatchDeleteDialogOpen(false) loadExpressions() loadStats() } catch (error) { toast({ title: '批量删除失败', description: error instanceof Error ? error.message : '无法批量删除表达方式', variant: 'destructive', }) } } // 页面跳转 const handleJumpToPage = () => { const targetPage = parseInt(jumpToPage) const totalPages = Math.ceil(total / pageSize) if (targetPage >= 1 && targetPage <= totalPages) { setPage(targetPage) setJumpToPage('') } else { toast({ title: '无效的页码', description: `请输入1-${totalPages}之间的页码`, variant: 'destructive', }) } } return (
{/* 页面标题 */}

表达方式管理

管理麦麦的表达方式和话术模板

{/* 统计卡片 */}
总数量
{stats.total}
近7天新增
{stats.recent_7days}
关联聊天数
{stats.chat_count}
{/* 搜索和批量操作 */}
setSearch(e.target.value)} className="pl-9" />
{/* 批量操作工具栏 */}
{selectedIds.size > 0 && ( 已选择 {selectedIds.size} 个表达方式 )}
{selectedIds.size > 0 && ( <> )}
{/* 表达方式列表 */}
{/* 桌面端表格视图 */}
0} onCheckedChange={toggleSelectAll} /> 情境 风格 聊天 操作 {loading ? ( 加载中... ) : expressions.length === 0 ? ( 暂无数据 ) : ( expressions.map((expression) => ( toggleSelect(expression.id)} /> {expression.situation} {expression.style} {getChatName(expression.chat_id)}
)) )}
{/* 移动端卡片视图 */}
{loading ? (
加载中...
) : expressions.length === 0 ? (
暂无数据
) : ( expressions.map((expression) => (
{/* 复选框和情境 */}
toggleSelect(expression.id)} className="mt-1" />
情境

{expression.situation}

风格

{expression.style}

{/* 聊天名称 */}
聊天

{getChatName(expression.chat_id)}

{/* 操作按钮 */}
)) )}
{/* 分页 - 增强版 */} {total > 0 && (
共 {total} 条记录,第 {page} / {Math.ceil(total / pageSize)} 页
{/* 首页 */} {/* 上一页 */} {/* 页码跳转 */}
setJumpToPage(e.target.value)} onKeyDown={(e) => e.key === 'Enter' && handleJumpToPage()} placeholder={page.toString()} className="w-16 h-8 text-center" min={1} max={Math.ceil(total / pageSize)} />
{/* 下一页 */} {/* 末页 */}
)}
{/* 详情对话框 */} {/* 创建对话框 */} { loadExpressions() loadStats() setIsCreateDialogOpen(false) }} /> {/* 编辑对话框 */} { loadExpressions() loadStats() setIsEditDialogOpen(false) }} /> {/* 删除确认对话框 */} setDeleteConfirmExpression(null)} > 确认删除 确定要删除表达方式 "{deleteConfirmExpression?.situation}" 吗? 此操作不可撤销。 取消 deleteConfirmExpression && handleDelete(deleteConfirmExpression)} className="bg-destructive text-destructive-foreground hover:bg-destructive/90" > 删除 {/* 批量删除确认对话框 */} {/* 表达方式审核器 */} { setIsReviewerOpen(open) if (!open) { // 关闭审核器时刷新列表和统计 loadExpressions() loadStats() loadReviewStats() } }} />
) } // 表达方式详情对话框 function ExpressionDetailDialog({ expression, open, onOpenChange, chatNameMap, }: { expression: Expression | null open: boolean onOpenChange: (open: boolean) => void chatNameMap: Map }) { if (!expression) return null const formatTime = (timestamp: number | null) => { if (!timestamp) return '-' return new Date(timestamp * 1000).toLocaleString('zh-CN') } const getChatName = (chatId: string): string => { return chatNameMap.get(chatId) || chatId } return ( 表达方式详情 查看表达方式的完整信息
{/* 状态标记 */}
{expression.checked ? ( ) : ( )}

已检查

{expression.checked ? "已通过审核" : "未审核"}

{expression.rejected ? ( ) : ( )}

已拒绝

{expression.rejected ? "不会被使用" : "正常"}

) } // 信息项组件 function InfoItem({ icon: Icon, label, value, mono = false, }: { icon?: typeof Hash label: string value: string | null | undefined mono?: boolean }) { return (
{value || '-'}
) } // 表达方式创建对话框 function ExpressionCreateDialog({ open, onOpenChange, chatList, onSuccess, }: { open: boolean onOpenChange: (open: boolean) => void chatList: ChatInfo[] onSuccess: () => void }) { const [formData, setFormData] = useState({ situation: '', style: '', chat_id: '', }) const [saving, setSaving] = useState(false) const { toast } = useToast() const handleCreate = async () => { if (!formData.situation || !formData.style || !formData.chat_id) { toast({ title: '验证失败', description: '请填写必填字段:情境、风格和聊天', variant: 'destructive', }) return } try { setSaving(true) await createExpression(formData) toast({ title: '创建成功', description: '表达方式已创建', }) // 重置表单 setFormData({ situation: '', style: '', chat_id: '', }) onSuccess() } catch (error) { toast({ title: '创建失败', description: error instanceof Error ? error.message : '无法创建表达方式', variant: 'destructive', }) } finally { setSaving(false) } } return ( 新增表达方式 创建新的表达方式记录
setFormData({ ...formData, situation: e.target.value })} placeholder="描述使用场景" />
setFormData({ ...formData, style: e.target.value })} placeholder="描述表达风格" />
) } // 表达方式编辑对话框 function ExpressionEditDialog({ expression, open, onOpenChange, chatList, onSuccess, }: { expression: Expression | null open: boolean onOpenChange: (open: boolean) => void chatList: ChatInfo[] onSuccess: () => void }) { const [formData, setFormData] = useState({}) const [saving, setSaving] = useState(false) const { toast } = useToast() useEffect(() => { if (expression) { setFormData({ situation: expression.situation, style: expression.style, chat_id: expression.chat_id, checked: expression.checked, rejected: expression.rejected, }) } }, [expression]) const handleSave = async () => { if (!expression) return try { setSaving(true) await updateExpression(expression.id, formData) toast({ title: '保存成功', description: '表达方式已更新', }) onSuccess() } catch (error) { toast({ title: '保存失败', description: error instanceof Error ? error.message : '无法更新表达方式', variant: 'destructive', }) } finally { setSaving(false) } } if (!expression) return null return ( 编辑表达方式 修改表达方式的信息
setFormData({ ...formData, situation: e.target.value })} placeholder="描述使用场景" />
setFormData({ ...formData, style: e.target.value })} placeholder="描述表达风格" />
{/* 状态标记 */}

状态标记说明:

• 已检查:表示该表达方式已通过审核(可由AI自动检查或人工审核)

• 已拒绝:表示该表达方式被标记为不合适,将永远不会被使用

根据配置中"仅使用已审核通过的表达方式"设置:
• 开启时:只有通过审核(已检查)的项目会被使用
• 关闭时:未审核的项目也会被使用

已通过审核

setFormData({ ...formData, checked })} />

不会被使用

setFormData({ ...formData, rejected })} />
) } // 批量删除确认对话框 function BatchDeleteConfirmDialog({ open, onOpenChange, onConfirm, count, }: { open: boolean onOpenChange: (open: boolean) => void onConfirm: () => void count: number }) { return ( 确认批量删除 您即将删除 {count} 个表达方式,此操作无法撤销。确定要继续吗? 取消 确认删除 ) }