import React, { useEffect, useRef, useState } from 'react'; import { XCard, registerCatalog, type ActionPayload, type XAgentCommand_v0_9, } from '@ant-design/x-card'; import { Alert, Button, Flex, Popconfirm, Steps, Table, Tag, Typography, type TableProps, } from 'antd'; import type { AiReviewRow, AiReviewSchema, AiReviewSection, AiReviewSectionType } from './types'; import { useXCardSurface } from './useSubmissionState'; import { GROUP_STATUS_LABELS, SECTION_ORDER, SECTION_STATUS_LABELS, SECTION_TYPE_LABELS, dependencyHint, groupSections, groupStatus, sectionCount, sectionResultText, sectionStatus, sectionType, } from './reviewSection'; const REVIEW_CATALOG_ID = 'gongxue-review-catalog'; registerCatalog({ catalogId: REVIEW_CATALOG_ID, components: { ReviewPreview: { type: 'object', properties: { review: { type: 'object' }, disabled: { type: 'boolean' }, activeKey: { type: 'string' }, activeType: { type: 'string' }, submittingKey: { type: ['string', 'null'] }, submittingGroup: { type: 'boolean' }, error: { type: ['string', 'null'] }, }, }, }, }); function surfaceId(reviewId: string): string { return `review-${reviewId}`; } function errorMessage(reason: unknown): string { if (reason instanceof Error) return reason.message; if (reason && typeof reason === 'object' && 'message' in reason) { return String((reason as { message?: unknown }).message ?? '确认失败,请稍后重试'); } return '确认失败,请稍后重试'; } function SectionTable({ section }: { section: AiReviewSection }) { const columns: TableProps['columns'] = section.columns.map((column) => ({ title: column.title, dataIndex: column.key, key: column.key, ellipsis: true, render: (value: unknown) => value === null || value === undefined || value === '' ? ( - ) : ( String(value) ), })); return ( size="small" rowKey="__rowKey" columns={columns} dataSource={section.rows.map((row, index) => ({ ...row, __rowKey: `row-${index}` }))} pagination={{ pageSize: 10, size: 'small', hideOnSinglePage: true }} scroll={{ x: 'max-content' }} /> ); } interface ReviewPreviewProps { review?: AiReviewSchema & { submitting?: boolean; activeKey?: string; activeType?: AiReviewSectionType; submittingKey?: string | null; submittingGroup?: boolean; error?: string | null; }; disabled?: boolean; onAction?: (name: string, context: Record) => void; } const ReviewPreview: React.FC = ({ review, disabled, onAction }) => { if (!review) return null; const submitted = review.status === 'submitted'; const expired = review.status === 'expired'; const submitting = Boolean(review.submitting); const submittingKey = review.submittingKey ?? null; const submittingGroup = Boolean(review.submittingGroup); const sections = review.sections; const presentTypes = SECTION_ORDER.filter((type) => sections.some((section) => sectionType(section) === type), ); const activeType = presentTypes.includes(review.activeType as AiReviewSectionType) ? (review.activeType as AiReviewSectionType) : presentTypes[0]; if (!activeType) return null; const activeSection = sections.find((section) => section.key === review.activeKey) ?? groupSections(sections, activeType)[0]; const activeStatus = activeSection ? sectionStatus(activeSection) : 'pending'; const dependency = activeSection === undefined ? null : dependencyHint(sections, sectionType(activeSection)); const typeItems = presentTypes.map((type, index) => { const items = groupSections(sections, type); const status = groupStatus(sections, type, submittingKey, submittingGroup, activeType); const stepStatus: 'finish' | 'error' | 'process' | 'wait' = status === 'submitted' ? 'finish' : status === 'failed' ? 'error' : status === 'importing' || type === activeType ? 'process' : 'wait'; return { key: type, title: `${SECTION_TYPE_LABELS[type]}(${items.reduce((sum, item) => sum + sectionCount(item), 0)})`, content: GROUP_STATUS_LABELS[status], status: stepStatus, index, }; }); const group = groupSections(sections, activeType); const typeTotal = group.reduce((sum, section) => sum + sectionCount(section), 0); const groupDep = dependencyHint(sections, activeType); const groupReady = !submitted && !expired && !disabled && !submitting && !submittingKey && !submittingGroup && group.length > 0 && !group.every((section) => sectionStatus(section) === 'submitted') && !groupDep; const anyRunning = submitting || Boolean(submittingKey) || submittingGroup; const allIssues = sections.flatMap((section) => section.issues); const allRows = sections.reduce((sum, section) => sum + sectionCount(section), 0); return (
{review.title} {submitted ? ( 已导入 ) : expired ? ( 已失效 ) : anyRunning ? ( 导入中 ) : ( 待确认 )} {review.summary && ( {review.summary} )} {expired && ( )} item.key === activeType), )} items={typeItems.map((item) => ({ key: item.key, title: item.title, content: item.content, status: item.status, }))} onChange={(index) => { const type = typeItems[index]?.key; if (type) onAction?.('review:selectType', { type }); }} /> {activeType && ( {SECTION_TYPE_LABELS[activeType]} · 共 {group.length} 张表 / {typeTotal} 行 { GROUP_STATUS_LABELS[ groupStatus(sections, activeType, submittingKey, submittingGroup, activeType) ] } {groupDep && ( )} {!submitted && !expired && group.length > 0 && ( onAction?.('review:confirmGroup', { reviewId: review.id, type: activeType, }) } > )} {group.map((section, index) => { const status = sectionStatus(section); const dep = dependencyHint(sections, sectionType(section)); const canConfirm = !submitted && !expired && !disabled && !anyRunning && status !== 'submitted' && status !== 'skipped' && !dep; return ( onAction?.('review:selectStep', { sectionKey: section.key })} > {index + 1}. {section.title} {section.sheet ? ( ({section.sheet}) ) : null} {sectionCount(section)} 行 · {SECTION_STATUS_LABELS[status]} ); })} {activeSection && ( {activeSection.issues.length > 0 && ( {activeSection.issues.slice(0, 20).map((issue, issueIndex) => (
  • {issue}
  • ))} } /> )} {dependency && ( )} {activeStatus === 'failed' && ( )} {activeSection.resultSummary && activeStatus === 'submitted' && ( {sectionResultText(activeSection)} )}
    )}
    )} 共 {allRows} 行,含 {allIssues.length} 条提示 {!submitted && !expired && ( onAction?.('review:submit', { reviewId: review.id })} > )} {submitted && } {review.error && ( )}
    ); }; export interface DynamicReviewProps { review: AiReviewSchema; disabled?: boolean; messageId?: number; onSubmit: (reviewId: string) => void | Promise; onConfirmStep?: ( messageId: number | undefined, reviewId: string, sectionKey: string, ) => AiReviewSchema | Promise | void; onConfirmGroup?: ( messageId: number | undefined, reviewId: string, type: AiReviewSectionType, ) => AiReviewSchema | Promise | void; } /** * Batch-import review card rendered through the official A2UI renderer * (@ant-design/x-card). Sections are grouped by business type; each sheet is * confirmed independently, the whole type group can be confirmed together, or * everything can be confirmed in one flow. */ export const DynamicReview: React.FC = ({ review, disabled, messageId, onSubmit, onConfirmStep, onConfirmGroup, }) => { const [submitting, setSubmitting] = useState(false); const [submittingKey, setSubmittingKey] = useState(null); const [submittingGroup, setSubmittingGroup] = useState(false); const [activeKey, setActiveKey] = useState(undefined); const [activeType, setActiveType] = useState(undefined); const activeTypeRef = useRef(activeType); activeTypeRef.current = activeType; const [localReview, setLocalReview] = useState(review); const [error, setError] = useState(null); const sid = surfaceId(localReview.id); const { commands, pushCommands } = useXCardSurface(sid); useEffect(() => { setLocalReview(review); const types = SECTION_ORDER.filter((type) => review.sections.some((section) => sectionType(section) === type), ); const preferredType = activeTypeRef.current && types.includes(activeTypeRef.current) ? activeTypeRef.current : types[0]; setActiveType(preferredType); setActiveKey((current) => current && review.sections.some( (section) => section.key === current && sectionType(section) === preferredType, ) ? current : review.sections.find((section) => sectionType(section) === preferredType)?.key, ); }, [review]); useEffect(() => { const cmds: XAgentCommand_v0_9[] = [ { version: 'v0.9', createSurface: { surfaceId: sid, catalogId: REVIEW_CATALOG_ID }, }, { version: 'v0.9', updateDataModel: { surfaceId: sid, path: '/review', value: { ...localReview, submitting, activeKey, activeType, submittingKey, submittingGroup, error, }, }, }, { version: 'v0.9', updateComponents: { surfaceId: sid, components: [ { id: 'root', component: 'ReviewPreview', review: { path: '/review' }, disabled: Boolean(disabled), }, ], }, }, ]; pushCommands(cmds); }, [ activeKey, activeType, disabled, error, localReview, pushCommands, sid, submitting, submittingGroup, submittingKey, ]); const handleSubmit = async (reviewId: string) => { if (submitting) return; setSubmitting(true); setError(null); try { await onSubmit(reviewId); } catch (reason) { setError(errorMessage(reason)); } finally { setSubmitting(false); } }; const handleConfirmStep = async (reviewId: string, sectionKey: string) => { if (submittingKey) return; setSubmittingKey(sectionKey); setError(null); try { const updated = await onConfirmStep?.(messageId, reviewId, sectionKey); if (updated) setLocalReview(updated); } catch (reason) { setError(errorMessage(reason)); } finally { setSubmittingKey(null); } }; const handleConfirmGroup = async (reviewId: string, type: AiReviewSectionType) => { if (submittingGroup) return; setSubmittingGroup(true); setError(null); try { const updated = await onConfirmGroup?.(messageId, reviewId, type); if (updated) setLocalReview(updated); } catch (reason) { setError(errorMessage(reason)); } finally { setSubmittingGroup(false); } }; const handleAction = (payload: ActionPayload) => { const context = payload.context ?? {}; if (payload.name === 'review:submit') { const reviewId = typeof context.reviewId === 'string' ? context.reviewId : localReview.id; void handleSubmit(reviewId); return; } if (payload.name === 'review:selectType') { const type = context.type as AiReviewSectionType | undefined; if (type && SECTION_ORDER.includes(type)) { setActiveType(type); setActiveKey(localReview.sections.find((section) => sectionType(section) === type)?.key); } return; } if (payload.name === 'review:selectStep') { if (typeof context.sectionKey === 'string') { const section = localReview.sections.find((item) => item.key === context.sectionKey); setActiveKey(context.sectionKey); if (section) setActiveType(sectionType(section)); } return; } if (payload.name === 'review:confirmStep') { const reviewId = typeof context.reviewId === 'string' ? context.reviewId : localReview.id; if (typeof context.sectionKey === 'string') { void handleConfirmStep(reviewId, context.sectionKey); } return; } if (payload.name === 'review:confirmGroup') { const reviewId = typeof context.reviewId === 'string' ? context.reviewId : localReview.id; const type = context.type as AiReviewSectionType | undefined; if (type && SECTION_ORDER.includes(type)) { void handleConfirmGroup(reviewId, type); } } }; return (
    {error && }
    ); };