Your Name 0ef7650c09 feat: 审核体系全面改造 — 多维度评分 + 卖点优先级 + AI 语义匹配 + 品牌方 AI 状态通知
后端:
- 审核结果拆分为 4 个独立维度 (法规合规/平台规则/品牌安全/Brief匹配度)
- 卖点优先级从 required:bool 改为三级 (core/recommended/reference)
- AI 语义匹配卖点覆盖 + AI 整体 Brief 匹配度分析
- BriefMatchDetail 评分详情 (覆盖率+亮点+问题点)
- min_selling_points 代理商可配置最少卖点数 + Alembic 迁移
- AI 语境复核过滤误报
- Brief AI 解析 + 规则 AI 解析
- AI 未配置/异常时通知品牌方
- 种子数据更新 (新格式审核结果+brief_match_detail)

前端:
- 三端审核页面展示四维度评分卡片
- 卖点编辑改为三级优先级选择器
- BriefMatchDetail 展示 (覆盖率进度条+亮点+问题)
- min_selling_points 配置 UI
- AI 配置页未配置时静默处理
- 文件预览/下载/签名 URL 优化

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-11 19:11:54 +08:00

758 lines
28 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

'use client'
import { useState, useEffect, useCallback } from 'react'
import { useRouter, useParams } from 'next/navigation'
import { useToast } from '@/components/ui/Toast'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/Card'
import { Button } from '@/components/ui/Button'
import { Modal, ConfirmModal } from '@/components/ui/Modal'
import { SuccessTag, WarningTag, ErrorTag } from '@/components/ui/Tag'
import { ReviewSteps, getBrandReviewSteps } from '@/components/ui/ReviewSteps'
import {
ArrowLeft,
Play,
Pause,
AlertTriangle,
Shield,
Radio,
User,
Building,
Clock,
CheckCircle,
XCircle,
MessageSquare,
ExternalLink,
MessageSquareWarning,
Loader2,
} from 'lucide-react'
import { FileInfoCard, FilePreviewModal, type FileInfo } from '@/components/ui/FilePreview'
import { api } from '@/lib/api'
import { USE_MOCK } from '@/contexts/AuthContext'
import type { TaskResponse } from '@/types/task'
// ==================== AI 审核结果类型 ====================
interface AIReviewResult {
score: number
violations: Array<{
type: string
content: string
severity: string
suggestion: string
timestamp?: number
source?: string
}>
soft_warnings: Array<{
type: string
content: string
suggestion: string
}>
summary?: string
}
// ==================== 本地视图数据类型 ====================
interface VideoTaskView {
id: string
title: string
creatorName: string
agencyName: string
projectName: string
submittedAt: string
duration: number
aiScore: number
status: string
file: FileInfo
isAppeal: boolean
appealReason: string
agencyReview: {
reviewer: string
result: string
comment: string
reviewedAt?: string
}
hardViolations: Array<{
id: string
type: string
content: string
timestamp: number
source: string
riskLevel: string
aiConfidence: number
suggestion: string
}>
sentimentWarnings: Array<{
id: string
type: string
timestamp: number
content: string
riskLevel: string
}>
sellingPointsCovered: Array<{
point: string
covered: boolean
timestamp: number
}>
aiSummary?: string
}
// ==================== Mock 数据 ====================
const mockVideoTask: VideoTaskView = {
id: 'video-001',
title: '夏日护肤推广',
creatorName: '小美护肤',
agencyName: '星耀传媒',
projectName: 'XX品牌618推广',
submittedAt: '2026-02-06 15:00',
duration: 135,
aiScore: 85,
status: 'brand_reviewing',
file: {
id: 'file-video-001',
fileName: '夏日护肤_成片v2.mp4',
fileSize: '128 MB',
fileType: 'video/mp4',
fileUrl: '/demo/videos/video-001.mp4',
uploadedAt: '2026-02-06 15:00',
duration: '02:15',
thumbnail: '/demo/videos/video-001-thumb.jpg',
},
isAppeal: false,
appealReason: '',
agencyReview: {
reviewer: '张经理',
result: 'approved',
comment: '视频质量良好,发现的问题已确认为误报,建议通过。',
reviewedAt: '2026-02-06 16:00',
},
hardViolations: [
{
id: 'v1',
type: '违禁词',
content: '效果最好',
timestamp: 15.5,
source: 'speech',
riskLevel: 'high',
aiConfidence: 0.95,
suggestion: '建议替换为"效果显著"',
},
{
id: 'v2',
type: '竞品露出',
content: '疑似竞品Logo',
timestamp: 42.0,
source: 'visual',
riskLevel: 'medium',
aiConfidence: 0.72,
suggestion: '经代理商确认为背景杂物,非竞品',
},
],
sentimentWarnings: [
{ id: 's1', type: '表情预警', timestamp: 68.0, content: '表情过于夸张,可能引发不适', riskLevel: 'low' },
],
sellingPointsCovered: [
{ point: 'SPF50+ PA++++', covered: true, timestamp: 25.0 },
{ point: '轻薄质地', covered: true, timestamp: 38.0 },
{ point: '不油腻', covered: true, timestamp: 52.0 },
{ point: '延展性好', covered: true, timestamp: 45.0 },
],
}
// ==================== 工具函数 ====================
function formatTimestamp(seconds: number): string {
const mins = Math.floor(seconds / 60)
const secs = Math.floor(seconds % 60)
return `${mins}:${secs.toString().padStart(2, '0')}`
}
function formatDurationString(seconds: number): string {
const mins = Math.floor(seconds / 60)
const secs = Math.floor(seconds % 60)
return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`
}
function severityToRiskLevel(severity: string): string {
if (severity === 'high' || severity === 'critical') return 'high'
if (severity === 'medium') return 'medium'
return 'low'
}
/** 将后端 TaskResponse 映射为本地视图数据 */
function mapTaskToView(task: TaskResponse): VideoTaskView {
const aiResult = task.video_ai_result as AIReviewResult | null | undefined
const hardViolations = (aiResult?.violations || []).map((v, idx) => ({
id: `v${idx}`,
type: v.type,
content: v.content,
timestamp: v.timestamp ?? 0,
source: v.source ?? 'unknown',
riskLevel: severityToRiskLevel(v.severity),
aiConfidence: 0.9,
suggestion: v.suggestion,
}))
const sentimentWarnings = (aiResult?.soft_warnings || []).map((w, idx) => ({
id: `s${idx}`,
type: w.type,
timestamp: 0,
content: w.content,
riskLevel: 'low',
}))
const duration = task.video_duration || 0
return {
id: task.id,
title: task.name,
creatorName: task.creator.name,
agencyName: task.agency.name,
projectName: task.project.name,
submittedAt: task.video_uploaded_at || task.created_at,
duration,
aiScore: task.video_ai_score || 0,
status: task.stage,
file: {
id: task.id,
fileName: task.video_file_name || '视频文件',
fileSize: '',
fileType: 'video/mp4',
fileUrl: task.video_file_url || '',
uploadedAt: task.video_uploaded_at || task.created_at,
duration: formatDurationString(duration),
thumbnail: task.video_thumbnail_url || undefined,
},
isAppeal: task.is_appeal,
appealReason: task.appeal_reason || '',
agencyReview: {
reviewer: task.agency.name,
result: task.video_agency_status === 'passed' || task.video_agency_status === 'force_passed' ? 'approved' : (task.video_agency_status || 'pending'),
comment: task.video_agency_comment || '',
},
hardViolations,
sentimentWarnings,
// 卖点覆盖目前后端暂无,保留空数组
sellingPointsCovered: [],
aiSummary: aiResult?.summary,
}
}
// ==================== 子组件 ====================
function ReviewProgressBar({ taskStatus }: { taskStatus: string }) {
const steps = getBrandReviewSteps(taskStatus)
const currentStep = steps.find(s => s.status === 'current')
return (
<Card className="mb-6">
<CardContent className="py-4">
<div className="flex items-center justify-between mb-3">
<span className="text-sm font-medium text-text-primary"></span>
<span className="text-sm text-accent-indigo font-medium">
{currentStep?.label || '品牌方终审'}
</span>
</div>
<ReviewSteps steps={steps} />
</CardContent>
</Card>
)
}
function RiskLevelTag({ level }: { level: string }) {
if (level === 'high') return <ErrorTag></ErrorTag>
if (level === 'medium') return <WarningTag></WarningTag>
return <SuccessTag></SuccessTag>
}
function LoadingSkeleton() {
return (
<div className="space-y-4 animate-pulse">
{/* 顶部导航骨架 */}
<div className="flex items-center gap-4">
<div className="w-9 h-9 bg-bg-elevated rounded-full" />
<div className="flex-1 space-y-2">
<div className="h-6 w-48 bg-bg-elevated rounded" />
<div className="h-4 w-72 bg-bg-elevated rounded" />
</div>
</div>
{/* 流程进度骨架 */}
<div className="h-20 bg-bg-elevated rounded-xl" />
{/* 主体骨架 */}
<div className="grid grid-cols-1 lg:grid-cols-5 gap-6">
<div className="lg:col-span-3 space-y-4">
<div className="h-16 bg-bg-elevated rounded-xl" />
<div className="aspect-video bg-bg-elevated rounded-xl" />
<div className="h-32 bg-bg-elevated rounded-xl" />
<div className="h-24 bg-bg-elevated rounded-xl" />
</div>
<div className="lg:col-span-2 space-y-4">
<div className="h-48 bg-bg-elevated rounded-xl" />
<div className="h-32 bg-bg-elevated rounded-xl" />
<div className="h-40 bg-bg-elevated rounded-xl" />
</div>
</div>
</div>
)
}
// ==================== 主页面 ====================
export default function BrandVideoReviewPage() {
const router = useRouter()
const params = useParams()
const toast = useToast()
const taskId = params.id as string
const [task, setTask] = useState<VideoTaskView | null>(null)
const [loading, setLoading] = useState(true)
const [submitting, setSubmitting] = useState(false)
const [isPlaying, setIsPlaying] = useState(false)
const [showApproveModal, setShowApproveModal] = useState(false)
const [showRejectModal, setShowRejectModal] = useState(false)
const [rejectReason, setRejectReason] = useState('')
const [checkedViolations, setCheckedViolations] = useState<Record<string, boolean>>({})
const [showFilePreview, setShowFilePreview] = useState(false)
const [videoError, setVideoError] = useState(false)
// 加载任务数据
const loadTask = useCallback(async () => {
if (!taskId) return
if (USE_MOCK) {
// Mock 模式下使用静态数据
await new Promise((resolve) => setTimeout(resolve, 300))
setTask(mockVideoTask)
setLoading(false)
return
}
try {
setLoading(true)
const response = await api.getTask(taskId)
setTask(mapTaskToView(response))
} catch (err) {
const message = err instanceof Error ? err.message : '加载任务失败'
toast.error(message)
} finally {
setLoading(false)
}
}, [taskId, toast])
useEffect(() => {
loadTask()
}, [loadTask])
// 通过审核
const handleApprove = async () => {
if (submitting) return
setSubmitting(true)
try {
if (!USE_MOCK) {
await api.reviewVideo(taskId, { action: 'pass', comment: '' })
} else {
await new Promise((resolve) => setTimeout(resolve, 300))
}
setShowApproveModal(false)
toast.success('审核通过!')
router.push('/brand/review')
} catch (err) {
const message = err instanceof Error ? err.message : '操作失败'
toast.error(message)
} finally {
setSubmitting(false)
}
}
// 驳回审核
const handleReject = async () => {
if (!rejectReason.trim()) {
toast.error('请填写驳回原因')
return
}
if (submitting) return
setSubmitting(true)
try {
if (!USE_MOCK) {
await api.reviewVideo(taskId, { action: 'reject', comment: rejectReason })
} else {
await new Promise((resolve) => setTimeout(resolve, 300))
}
setShowRejectModal(false)
toast.success('已驳回')
router.push('/brand/review')
} catch (err) {
const message = err instanceof Error ? err.message : '操作失败'
toast.error(message)
} finally {
setSubmitting(false)
}
}
// 加载中状态
if (loading || !task) {
return <LoadingSkeleton />
}
// 计算问题时间点用于进度条展示
const timelineMarkers = [
...task.hardViolations.map(v => ({ time: v.timestamp, type: 'hard' as const })),
...task.sentimentWarnings.filter(w => w.timestamp > 0).map(w => ({ time: w.timestamp, type: 'soft' as const })),
...task.sellingPointsCovered.filter(s => s.covered).map(s => ({ time: s.timestamp, type: 'selling' as const })),
].sort((a, b) => a.time - b.time)
return (
<div className="space-y-4">
{/* 顶部导航 */}
<div className="flex items-center gap-4">
<button type="button" onClick={() => router.back()} className="p-2 hover:bg-bg-elevated rounded-full">
<ArrowLeft size={20} className="text-text-primary" />
</button>
<div className="flex-1">
<div className="flex items-center gap-2">
<h1 className="text-xl font-bold text-text-primary">{task.title}</h1>
{task.isAppeal && (
<span className="flex items-center gap-1 px-2 py-1 text-xs bg-accent-amber/20 text-accent-amber rounded-full font-medium">
<MessageSquareWarning size={12} />
</span>
)}
</div>
<div className="flex items-center gap-4 mt-1 text-sm text-text-secondary">
<span className="flex items-center gap-1">
<User size={14} />
{task.creatorName}
</span>
<span className="flex items-center gap-1">
<Building size={14} />
{task.agencyName}
</span>
<span className="flex items-center gap-1">
<Clock size={14} />
{task.submittedAt}
</span>
</div>
</div>
</div>
{/* 申诉理由 */}
{task.isAppeal && task.appealReason && (
<div className="p-4 rounded-xl bg-accent-amber/10 border border-accent-amber/30">
<p className="text-sm text-accent-amber font-medium mb-1 flex items-center gap-1">
<MessageSquareWarning size={14} />
</p>
<p className="text-text-secondary">{task.appealReason}</p>
</div>
)}
{/* 审核流程进度条 */}
<ReviewProgressBar taskStatus={task.status} />
<div className="grid grid-cols-1 lg:grid-cols-5 gap-6">
{/* 左侧:视频播放器 (3/5) */}
<div className="lg:col-span-3 space-y-4">
{/* 文件信息卡片 */}
<FileInfoCard
file={task.file}
onPreview={() => setShowFilePreview(true)}
/>
<Card>
<CardContent className="p-0">
{/* 真实视频播放器 */}
<div className="aspect-video bg-gray-900 rounded-t-lg overflow-hidden relative">
{videoError ? (
<div className="w-full h-full flex items-center justify-center">
<div className="text-center">
<Play size={48} className="mx-auto text-white/50 mb-3" />
<p className="text-white/70 mb-3"></p>
<Button
variant="secondary"
size="sm"
onClick={() => window.open(task.file.fileUrl, '_blank')}
>
<ExternalLink size={14} />
</Button>
</div>
</div>
) : (
<video
className="w-full h-full"
controls
poster={task.file.thumbnail}
onError={() => setVideoError(true)}
onPlay={() => setIsPlaying(true)}
onPause={() => setIsPlaying(false)}
>
<source src={task.file.fileUrl} type={task.file.fileType} />
</video>
)}
</div>
{/* 智能进度条 */}
{task.duration > 0 && (
<div className="p-4 border-t border-border-subtle">
<div className="text-sm font-medium text-text-primary mb-3"></div>
<div className="relative h-3 bg-bg-elevated rounded-full">
{/* 时间标记点 */}
{timelineMarkers.map((marker, idx) => (
<button
key={idx}
type="button"
className={`absolute top-1/2 -translate-y-1/2 w-4 h-4 rounded-full border-2 border-bg-card shadow-md cursor-pointer transition-transform hover:scale-125 ${
marker.type === 'hard' ? 'bg-accent-coral' : marker.type === 'soft' ? 'bg-orange-500' : 'bg-accent-green'
}`}
style={{ left: `${(marker.time / task.duration) * 100}%` }}
title={`${formatTimestamp(marker.time)} - ${marker.type === 'hard' ? '硬性问题' : marker.type === 'soft' ? '舆情提示' : '卖点覆盖'}`}
/>
))}
</div>
<div className="flex justify-between text-xs text-text-tertiary mt-1">
<span>0:00</span>
<span>{formatTimestamp(task.duration)}</span>
</div>
<div className="flex gap-4 mt-3 text-xs text-text-secondary">
<span className="flex items-center gap-1">
<span className="w-3 h-3 bg-accent-coral rounded-full" />
</span>
<span className="flex items-center gap-1">
<span className="w-3 h-3 bg-orange-500 rounded-full" />
</span>
<span className="flex items-center gap-1">
<span className="w-3 h-3 bg-accent-green rounded-full" />
</span>
</div>
</div>
)}
</CardContent>
</Card>
{/* 代理商初审意见 */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<MessageSquare size={18} className="text-blue-500" />
</CardTitle>
</CardHeader>
<CardContent>
<div className="flex items-start gap-4">
<div className={`p-2 rounded-full ${task.agencyReview.result === 'approved' ? 'bg-accent-green/20' : 'bg-accent-coral/20'}`}>
{task.agencyReview.result === 'approved' ? (
<CheckCircle size={20} className="text-accent-green" />
) : (
<XCircle size={20} className="text-accent-coral" />
)}
</div>
<div className="flex-1">
<div className="flex items-center gap-2 mb-1">
<span className="font-medium text-text-primary">{task.agencyReview.reviewer}</span>
{task.agencyReview.result === 'approved' ? (
<SuccessTag></SuccessTag>
) : (
<ErrorTag></ErrorTag>
)}
</div>
<p className="text-text-secondary text-sm">{task.agencyReview.comment || '暂无评论'}</p>
{task.agencyReview.reviewedAt && (
<p className="text-xs text-text-tertiary mt-2">{task.agencyReview.reviewedAt}</p>
)}
</div>
</div>
</CardContent>
</Card>
{/* AI 分析总结 */}
<Card>
<CardContent className="py-4">
<div className="flex items-center justify-between mb-2">
<span className="font-medium text-text-primary">AI </span>
<span className={`text-xl font-bold ${task.aiScore >= 80 ? 'text-accent-green' : 'text-yellow-400'}`}>
{task.aiScore}
</span>
</div>
<p className="text-text-secondary text-sm">
{task.aiSummary || `视频整体合规,发现${task.hardViolations.length}处硬性问题和${task.sentimentWarnings.length}处舆情提示,代理商已确认处理。`}
</p>
</CardContent>
</Card>
</div>
{/* 右侧AI 检查单 (2/5) */}
<div className="lg:col-span-2 space-y-4">
{/* 硬性合规 */}
<Card>
<CardHeader className="pb-2">
<CardTitle className="flex items-center gap-2 text-base">
<Shield size={16} className="text-red-500" />
({task.hardViolations.length})
</CardTitle>
</CardHeader>
<CardContent className="space-y-3">
{task.hardViolations.length === 0 && (
<p className="text-sm text-text-tertiary py-2"></p>
)}
{task.hardViolations.map((v) => (
<div key={v.id} className={`p-3 rounded-lg border ${checkedViolations[v.id] ? 'bg-bg-elevated border-border-subtle' : 'bg-accent-coral/10 border-accent-coral/30'}`}>
<div className="flex items-start gap-2">
<input
type="checkbox"
checked={checkedViolations[v.id] || false}
onChange={() => setCheckedViolations((prev) => ({ ...prev, [v.id]: !prev[v.id] }))}
className="mt-1 accent-accent-indigo"
/>
<div className="flex-1">
<div className="flex items-center gap-2 mb-1">
<ErrorTag>{v.type}</ErrorTag>
{v.timestamp > 0 && (
<span className="text-xs text-text-tertiary">{formatTimestamp(v.timestamp)}</span>
)}
</div>
<p className="text-sm font-medium text-text-primary">{v.content}</p>
<p className="text-xs text-accent-indigo mt-1">{v.suggestion}</p>
</div>
</div>
</div>
))}
</CardContent>
</Card>
{/* 舆情雷达 */}
{task.sentimentWarnings.length > 0 && (
<Card>
<CardHeader className="pb-2">
<CardTitle className="flex items-center gap-2 text-base">
<Radio size={16} className="text-orange-500" />
</CardTitle>
</CardHeader>
<CardContent className="space-y-2">
{task.sentimentWarnings.map((w) => (
<div key={w.id} className="p-3 bg-orange-500/10 rounded-lg border border-orange-500/30">
<div className="flex items-center gap-2 mb-1">
<WarningTag>{w.type}</WarningTag>
{w.timestamp > 0 && (
<span className="text-xs text-text-tertiary">{formatTimestamp(w.timestamp)}</span>
)}
</div>
<p className="text-sm text-orange-400">{w.content}</p>
<p className="text-xs text-text-tertiary mt-1"></p>
</div>
))}
</CardContent>
</Card>
)}
{/* 卖点覆盖 */}
{task.sellingPointsCovered.length > 0 && (
<Card>
<CardHeader className="pb-2">
<CardTitle className="flex items-center gap-2 text-base">
<CheckCircle size={16} className="text-accent-green" />
</CardTitle>
</CardHeader>
<CardContent className="space-y-2">
{task.sellingPointsCovered.map((sp, idx) => (
<div key={idx} className="flex items-center justify-between p-2 rounded-lg bg-bg-elevated">
<div className="flex items-center gap-2">
{sp.covered ? (
<CheckCircle size={16} className="text-accent-green" />
) : (
<XCircle size={16} className="text-accent-coral" />
)}
<span className="text-sm text-text-primary">{sp.point}</span>
</div>
{sp.covered && sp.timestamp > 0 && (
<span className="text-xs text-text-tertiary">{formatTimestamp(sp.timestamp)}</span>
)}
</div>
))}
</CardContent>
</Card>
)}
</div>
</div>
{/* 底部决策栏 */}
<Card className="sticky bottom-4 shadow-lg">
<CardContent className="py-4">
<div className="flex items-center justify-between">
<div className="text-sm text-text-secondary">
{Object.values(checkedViolations).filter(Boolean).length}/{task.hardViolations.length}
</div>
<div className="flex gap-3">
<Button variant="danger" onClick={() => setShowRejectModal(true)} disabled={submitting}>
{submitting ? <Loader2 size={16} className="animate-spin mr-1" /> : null}
</Button>
<Button variant="success" onClick={() => setShowApproveModal(true)} disabled={submitting}>
{submitting ? <Loader2 size={16} className="animate-spin mr-1" /> : null}
</Button>
</div>
</div>
</CardContent>
</Card>
{/* 通过确认弹窗 */}
<ConfirmModal
isOpen={showApproveModal}
onClose={() => setShowApproveModal(false)}
onConfirm={handleApprove}
title="确认通过"
message="确定要通过此视频的审核吗?通过后达人将收到通知。"
confirmText={submitting ? '提交中...' : '确认通过'}
/>
{/* 驳回弹窗 */}
<Modal isOpen={showRejectModal} onClose={() => setShowRejectModal(false)} title="驳回审核">
<div className="space-y-4">
<p className="text-text-secondary text-sm"></p>
<div className="p-3 bg-bg-elevated rounded-lg">
<p className="text-sm font-medium text-text-primary mb-2"> ({Object.values(checkedViolations).filter(Boolean).length})</p>
{task.hardViolations.filter(v => checkedViolations[v.id]).map(v => (
<div key={v.id} className="text-sm text-text-secondary">- {v.type}: {v.content}</div>
))}
{Object.values(checkedViolations).filter(Boolean).length === 0 && (
<div className="text-sm text-text-tertiary"></div>
)}
</div>
<div>
<label className="block text-sm font-medium text-text-primary mb-1"></label>
<textarea
className="w-full h-24 p-3 border border-border-subtle rounded-lg resize-none bg-bg-elevated text-text-primary focus:outline-none focus:ring-2 focus:ring-accent-indigo"
placeholder="请详细说明驳回原因..."
value={rejectReason}
onChange={(e) => setRejectReason(e.target.value)}
/>
</div>
<div className="flex gap-3 justify-end">
<Button variant="ghost" onClick={() => setShowRejectModal(false)} disabled={submitting}></Button>
<Button variant="danger" onClick={handleReject} disabled={submitting}>
{submitting ? <Loader2 size={16} className="animate-spin mr-1" /> : null}
</Button>
</div>
</div>
</Modal>
{/* 文件预览弹窗 */}
<FilePreviewModal
file={task.file}
isOpen={showFilePreview}
onClose={() => setShowFilePreview(false)}
/>
</div>
)
}