import { ArrowLeft, CheckCircle2, ImagePlus, Images, Loader2, Play, Plus, RefreshCw, Send, Trash2, } from 'lucide-react'; import { type ChangeEvent, useEffect, useMemo, useState } from 'react'; import { createPortal } from 'react-dom'; import type { SquareHoleResultDraft } from '../../../packages/shared/src/contracts/squareHoleAgent'; import type { PutSquareHoleWorkRequest, SquareHoleHoleOption, SquareHoleShapeOption, SquareHoleWorkProfile, } from '../../../packages/shared/src/contracts/squareHoleWorks'; import { publishSquareHoleWork, regenerateSquareHoleWorkImage, squareHoleAssetClient, type SquareHoleHistoryAsset, type SquareHoleImageAssetKind, updateSquareHoleWork, } from '../../services/square-hole-works'; import { useAuthUi } from '../auth/AuthUiContext'; import { PlatformActionButton } from '../common/PlatformActionButton'; import { PlatformAssetPickerGrid } from '../common/PlatformAssetPickerCard'; import { PlatformFieldLabel } from '../common/PlatformFieldLabel'; import { PlatformIconButton } from '../common/PlatformIconButton'; import { PlatformMediaFrame } from '../common/PlatformMediaFrame'; import { PlatformModalCloseButton } from '../common/PlatformModalCloseButton'; import { PlatformPillBadge } from '../common/PlatformPillBadge'; import { PlatformStatGrid } from '../common/PlatformStatGrid'; import { PlatformStatusMessage } from '../common/PlatformStatusMessage'; import { PlatformSubpanel } from '../common/PlatformSubpanel'; import { PlatformSelectField, PlatformTextField, } from '../common/PlatformTextField'; type SquareHoleResultViewProps = { profile: SquareHoleWorkProfile; draft?: SquareHoleResultDraft | null; isBusy?: boolean; error?: string | null; onBack: () => void; onSaved?: (profile: SquareHoleWorkProfile) => void; onPublished?: (profile: SquareHoleWorkProfile) => void; onStartTestRun: (profile: SquareHoleWorkProfile) => void; }; type SquareHoleAutoSaveState = 'idle' | 'saving' | 'saved' | 'error'; type SquareHoleResultEditState = { gameName: string; summary: string; tagsText: string; coverImageSrc: string; backgroundPrompt: string; backgroundImageSrc: string; themeText: string; twistRule: string; shapeOptions: SquareHoleShapeOption[]; holeOptions: SquareHoleHoleOption[]; shapeCountText: string; difficulty: number; }; type SquareHoleImageSlot = { kind: 'cover' | 'background' | 'shape' | 'hole'; title: string; assetKind: SquareHoleImageAssetKind; shapeOptionId?: string; holeOptionId?: string; }; const SQUARE_HOLE_AUTOSAVE_DEBOUNCE_MS = 600; const SQUARE_HOLE_SHAPE_KIND_OPTIONS = [ 'square', 'circle', 'triangle', 'star', 'arch', 'diamond', ]; function fallbackHoleOptionId(index: number) { return `hole-${index + 1}`; } function resolveFirstHoleId(holeOptions: SquareHoleHoleOption[]) { return ( holeOptions.find((option) => option.holeId.trim())?.holeId.trim() ?? '' ); } function resolveShapeTargetHoleId( option: SquareHoleShapeOption, index: number, holeOptions: SquareHoleHoleOption[], ) { const holeIds = holeOptions .map((holeOption) => holeOption.holeId.trim()) .filter(Boolean); const currentTarget = option.targetHoleId?.trim() ?? ''; if (holeIds.includes(currentTarget)) { return currentTarget; } return holeIds[index % Math.max(1, holeIds.length)] ?? ''; } function remapShapeTargetsToHoles( shapeOptions: SquareHoleShapeOption[], holeOptions: SquareHoleHoleOption[], ) { return shapeOptions.map((option, index) => ({ ...option, targetHoleId: resolveShapeTargetHoleId(option, index, holeOptions), })); } function normalizeTags(value: string) { return [ ...new Set( value .split(/[\n,,、]/u) .map((entry) => entry.trim()) .filter(Boolean), ), ]; } function normalizeShapeCount(value: string) { const parsed = Number.parseInt(value.trim(), 10); return Number.isFinite(parsed) && parsed >= 6 && parsed <= 24 ? parsed : null; } function createEditState( profile: SquareHoleWorkProfile, ): SquareHoleResultEditState { const holeOptions = profile.holeOptions.map((option, index) => { const holeId = option.holeId.trim() || fallbackHoleOptionId(index); return { ...option, holeId, holeKind: option.holeKind.trim() || holeId, imagePrompt: option.imagePrompt ?? '', imageSrc: option.imageSrc ?? null, }; }); const shapeOptions = remapShapeTargetsToHoles( profile.shapeOptions.map((option) => ({ ...option, targetHoleId: option.targetHoleId ?? '', })), holeOptions, ); return { gameName: profile.gameName, summary: profile.summary, tagsText: profile.tags.join(','), coverImageSrc: profile.coverImageSrc?.trim() || '', backgroundPrompt: profile.backgroundPrompt || '', backgroundImageSrc: profile.backgroundImageSrc?.trim() || '', themeText: profile.themeText, twistRule: profile.twistRule, shapeOptions, holeOptions, shapeCountText: String(profile.shapeCount), difficulty: profile.difficulty, }; } function buildSavePayload( editState: SquareHoleResultEditState, ): PutSquareHoleWorkRequest | null { const shapeCount = normalizeShapeCount(editState.shapeCountText); const gameName = editState.gameName.trim(); const themeText = editState.themeText.trim(); const twistRule = editState.twistRule.trim(); const summary = editState.summary.trim(); const tags = normalizeTags(editState.tagsText); const shapeOptions = editState.shapeOptions .map((option) => ({ optionId: option.optionId.trim(), shapeKind: option.shapeKind.trim(), label: option.label.trim(), targetHoleId: option.targetHoleId.trim(), imagePrompt: option.imagePrompt.trim(), imageSrc: option.imageSrc?.trim() || null, })) .filter( (option) => option.optionId && option.shapeKind && option.label && option.targetHoleId && option.imagePrompt, ); const holeOptions = editState.holeOptions .map((option, index) => { const holeId = option.holeId.trim() || fallbackHoleOptionId(index); return { holeId, holeKind: holeId, label: option.label.trim(), imagePrompt: option.imagePrompt.trim(), imageSrc: option.imageSrc?.trim() || null, }; }) .filter((option) => option.holeId && option.label && option.imagePrompt); const holeIdSet = new Set(holeOptions.map((option) => option.holeId)); const hasInvalidShapeTarget = shapeOptions.some( (option) => !holeIdSet.has(option.targetHoleId), ); if ( !gameName || !themeText || !twistRule || !summary || tags.length === 0 || shapeOptions.length === 0 || holeOptions.length === 0 || hasInvalidShapeTarget || !shapeCount ) { return null; } return { gameName, themeText, twistRule, summary, tags, coverImageSrc: editState.coverImageSrc.trim() || null, backgroundPrompt: editState.backgroundPrompt.trim(), backgroundImageSrc: editState.backgroundImageSrc.trim() || null, shapeOptions, holeOptions, shapeCount, difficulty: editState.difficulty, }; } function buildPublishBlockers(editState: SquareHoleResultEditState) { const blockers = [ ...(editState.gameName.trim() ? [] : ['游戏名称不能为空。']), ...(editState.themeText.trim() ? [] : ['题材主题不能为空。']), ...(editState.twistRule.trim() ? [] : ['反差规则不能为空。']), ...(editState.summary.trim() ? [] : ['简介不能为空。']), ...(normalizeTags(editState.tagsText).length > 0 ? [] : ['至少需要 1 个标签。']), ...(editState.shapeOptions.some( (option) => option.optionId.trim() && option.shapeKind.trim() && option.label.trim() && option.targetHoleId.trim() && option.imagePrompt.trim(), ) ? [] : ['至少需要 1 个形状选项。']), ...(editState.holeOptions.some( (option) => option.holeId.trim() && option.label.trim() && option.imagePrompt.trim(), ) ? [] : ['至少需要 1 个洞口选项。']), ...(editState.shapeOptions.every((option) => editState.holeOptions.some( (holeOption) => holeOption.holeId.trim() === option.targetHoleId.trim(), ), ) ? [] : ['每个展示选项都需要绑定一个洞口选项。']), ...(normalizeShapeCount(editState.shapeCountText) ? [] : ['形状数量需要在 6 到 24 之间。']), ]; return [...new Set(blockers)]; } function readImageAsDataUrl(file: File) { return new Promise((resolve, reject) => { if (!file.type.startsWith('image/')) { reject(new Error('请选择图片文件。')); return; } const reader = new FileReader(); reader.onerror = () => reject(new Error('封面图读取失败,请重试。')); reader.onload = () => resolve(String(reader.result || '')); reader.readAsDataURL(file); }); } function createShapeOption( index: number, holeOptions: SquareHoleHoleOption[], ): SquareHoleShapeOption { return { optionId: `shape-${Date.now().toString(36)}-${index}`, shapeKind: SQUARE_HOLE_SHAPE_KIND_OPTIONS[ index % SQUARE_HOLE_SHAPE_KIND_OPTIONS.length ] ?? 'square', label: `形状 ${index + 1}`, targetHoleId: resolveFirstHoleId(holeOptions), imagePrompt: '主题贴图', imageSrc: null, }; } function createHoleOption(index: number): SquareHoleHoleOption { const holeId = `hole-${Date.now().toString(36)}-${index}`; return { holeId, holeKind: holeId, label: `洞口 ${index + 1}`, imagePrompt: `洞口 ${index + 1}贴图,透明背景,游戏资产`, imageSrc: null, }; } function formatHistoryAssetDate(value: string) { const date = new Date(value); if (Number.isNaN(date.getTime())) { return value || ''; } return date.toLocaleString('zh-CN', { month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', }); } function resolveImageSlotSrc( editState: SquareHoleResultEditState, slot: SquareHoleImageSlot, ) { if (slot.kind === 'cover') { return editState.coverImageSrc; } if (slot.kind === 'background') { return editState.backgroundImageSrc; } if (slot.kind === 'hole') { return ( editState.holeOptions .find((option) => option.holeId === slot.holeOptionId) ?.imageSrc?.trim() || '' ); } return ( editState.shapeOptions .find((option) => option.optionId === slot.shapeOptionId) ?.imageSrc?.trim() || '' ); } function applyImageSlotSrc( current: SquareHoleResultEditState, slot: SquareHoleImageSlot, imageSrc: string, ): SquareHoleResultEditState { if (slot.kind === 'cover') { return { ...current, coverImageSrc: imageSrc, }; } if (slot.kind === 'background') { return { ...current, backgroundImageSrc: imageSrc, }; } if (slot.kind === 'hole') { return { ...current, holeOptions: current.holeOptions.map((option) => option.holeId === slot.holeOptionId ? { ...option, imageSrc, } : option, ), }; } return { ...current, shapeOptions: current.shapeOptions.map((option) => option.optionId === slot.shapeOptionId ? { ...option, imageSrc, } : option, ), }; } function buildPlayableProfile( profile: SquareHoleWorkProfile, editState: SquareHoleResultEditState, ) { const payload = buildSavePayload(editState); if (!payload) { return profile; } return { ...profile, gameName: payload.gameName, themeText: payload.themeText ?? profile.themeText, twistRule: payload.twistRule, summary: payload.summary, tags: payload.tags, coverImageSrc: payload.coverImageSrc, backgroundPrompt: payload.backgroundPrompt ?? profile.backgroundPrompt, backgroundImageSrc: payload.backgroundImageSrc, shapeOptions: payload.shapeOptions ?? profile.shapeOptions, holeOptions: payload.holeOptions ?? profile.holeOptions, shapeCount: payload.shapeCount, difficulty: payload.difficulty ?? profile.difficulty, }; } function SquareHoleResultHeader({ autoSaveState, isBusy, onBack, }: { autoSaveState: SquareHoleAutoSaveState; isBusy: boolean; onBack: () => void; }) { const badge = autoSaveState === 'saving' ? ( 保存中 ) : autoSaveState === 'saved' ? ( 已自动保存 ) : autoSaveState === 'error' ? ( 保存失败 ) : null; return (
返回 {badge}
); } function SquareHoleImageSlotDialog({ currentImageSrc, canRegenerateImages, isBusy, isRegeneratingImages, slot, onClose, onRegenerateImages, onSelectHistory, onUpload, }: { currentImageSrc: string; canRegenerateImages: boolean; isBusy: boolean; isRegeneratingImages: boolean; slot: SquareHoleImageSlot; onClose: () => void; onRegenerateImages: () => void; onSelectHistory: (asset: SquareHoleHistoryAsset) => void; onUpload: ( slot: SquareHoleImageSlot, event: ChangeEvent, ) => void; }) { const platformTheme = useAuthUi()?.platformTheme ?? 'light'; const [assets, setAssets] = useState([]); const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState(null); useEffect(() => { let cancelled = false; setIsLoading(true); setError(null); squareHoleAssetClient .listHistoryAssets({ kind: slot.assetKind, limit: 120 }) .then((nextAssets) => { if (!cancelled) { setAssets(nextAssets); } }) .catch((loadError) => { if (!cancelled) { setError( loadError instanceof Error ? loadError.message : '历史图片读取失败。', ); } }) .finally(() => { if (!cancelled) { setIsLoading(false); } }); return () => { cancelled = true; }; }, [slot.assetKind]); if (typeof document === 'undefined') { return null; } return createPortal(
{ if (event.target === event.currentTarget) { onClose(); } }} >
event.stopPropagation()} >
{slot.title}
} aspect="standard" surface="plain" className="rounded-[1.35rem]" fallbackClassName="tracking-normal text-[var(--platform-text-soft)]" />
上传图片 onUpload(slot, event)} /> {isRegeneratingImages ? ( ) : ( )} AI生成图片
历史生成 asset.assetObjectId} getImageSrc={(asset) => asset.imageSrc} getImageAlt={(asset) => asset.ownerLabel || '历史图片'} getTitle={(asset) => asset.ownerLabel || '未记录账号'} getSubtitle={(asset) => formatHistoryAssetDate(asset.createdAt)} onSelect={onSelectHistory} gridClassName="grid max-h-[24rem] grid-cols-2 gap-3 overflow-y-auto pr-1 sm:grid-cols-3 lg:grid-cols-2 xl:grid-cols-3" />
, document.body, ); } export function SquareHoleResultView({ profile, draft = null, isBusy = false, error = null, onBack, onSaved, onPublished, onStartTestRun, }: SquareHoleResultViewProps) { const [editState, setEditState] = useState(() => createEditState(profile)); const [autoSaveState, setAutoSaveState] = useState('idle'); const [localError, setLocalError] = useState(null); const [isPublishing, setIsPublishing] = useState(false); const [isRegeneratingImages, setIsRegeneratingImages] = useState(false); const [isApplyingHistoryImage, setIsApplyingHistoryImage] = useState(false); const [isStartingTestRun, setIsStartingTestRun] = useState(false); const [activeImageSlot, setActiveImageSlot] = useState(null); const blockers = useMemo(() => buildPublishBlockers(editState), [editState]); const canSubmit = blockers.length === 0; useEffect(() => { setEditState(createEditState(profile)); setAutoSaveState('idle'); setLocalError(null); }, [profile]); useEffect(() => { if ( isApplyingHistoryImage || isPublishing || isRegeneratingImages || isStartingTestRun ) { return undefined; } const payload = buildSavePayload(editState); if (!payload) { return undefined; } const currentPayload = buildSavePayload(createEditState(profile)); const changed = !currentPayload || payload.gameName !== currentPayload.gameName || payload.themeText !== currentPayload.themeText || payload.twistRule !== currentPayload.twistRule || payload.summary !== currentPayload.summary || (payload.coverImageSrc ?? '') !== (currentPayload.coverImageSrc ?? '') || (payload.backgroundPrompt ?? '') !== (currentPayload.backgroundPrompt ?? '') || (payload.backgroundImageSrc ?? '') !== (currentPayload.backgroundImageSrc ?? '') || payload.shapeCount !== currentPayload.shapeCount || JSON.stringify(payload.shapeOptions ?? []) !== JSON.stringify(currentPayload.shapeOptions ?? []) || JSON.stringify(payload.holeOptions ?? []) !== JSON.stringify(currentPayload.holeOptions ?? []) || payload.tags.length !== currentPayload.tags.length || payload.tags.some((tag, index) => tag !== currentPayload.tags[index]); if (!changed) { return undefined; } setAutoSaveState('saving'); setLocalError(null); let cancelled = false; const timer = window.setTimeout(() => { void updateSquareHoleWork(profile.profileId, payload) .then(({ item }) => { if (cancelled) { return; } setAutoSaveState('saved'); onSaved?.(item); }) .catch((saveError) => { if (cancelled) { return; } setAutoSaveState('error'); setLocalError( saveError instanceof Error ? saveError.message : '自动保存失败。', ); }); }, SQUARE_HOLE_AUTOSAVE_DEBOUNCE_MS); return () => { cancelled = true; window.clearTimeout(timer); }; }, [ editState, isApplyingHistoryImage, isPublishing, isRegeneratingImages, isStartingTestRun, onSaved, profile, ]); const saveStateNow = async (state: SquareHoleResultEditState) => { const payload = buildSavePayload(state); if (!payload) { setLocalError(blockers[0] ?? '请补全作品信息。'); return null; } setAutoSaveState('saving'); setLocalError(null); const { item } = await updateSquareHoleWork(profile.profileId, payload); setAutoSaveState('saved'); onSaved?.(item); return item; }; const saveNow = async () => { return saveStateNow(editState); }; const handleImageSlotUpload = async ( slot: SquareHoleImageSlot, event: ChangeEvent, ) => { const file = event.target.files?.[0] ?? null; event.target.value = ''; if (!file) { return; } try { const dataUrl = await readImageAsDataUrl(file); setEditState((current) => applyImageSlotSrc(current, slot, dataUrl)); setLocalError(null); } catch (caughtError) { setLocalError( caughtError instanceof Error ? caughtError.message : '图片读取失败。', ); } }; const handleStartTestRun = async () => { if (!canSubmit || isStartingTestRun) { setLocalError(blockers[0] ?? null); return; } setIsStartingTestRun(true); try { const savedProfile = await saveNow(); onStartTestRun(savedProfile ?? buildPlayableProfile(profile, editState)); } catch (caughtError) { setLocalError( caughtError instanceof Error ? caughtError.message : '启动试玩前保存失败。', ); } finally { setIsStartingTestRun(false); } }; const handleRegenerateImages = async () => { if (!activeImageSlot || !canSubmit || isRegeneratingImages) { setLocalError(blockers[0] ?? null); return; } setIsRegeneratingImages(true); try { const savedProfile = await saveNow(); const targetProfile = savedProfile ?? buildPlayableProfile(profile, editState); const { item } = await regenerateSquareHoleWorkImage( targetProfile.profileId, { visualAssetSlot: activeImageSlot.kind, visualAssetOptionId: activeImageSlot.shapeOptionId ?? activeImageSlot.holeOptionId ?? null, }, ); setEditState(createEditState(item)); onSaved?.(item); setLocalError(null); } catch (caughtError) { setLocalError( caughtError instanceof Error ? caughtError.message : '重生成图片前保存失败。', ); } finally { setIsRegeneratingImages(false); } }; const handleSelectHistoryImage = async ( slot: SquareHoleImageSlot, asset: SquareHoleHistoryAsset, ) => { if ( isApplyingHistoryImage || isBusy || isPublishing || isRegeneratingImages || isStartingTestRun ) { return; } const nextState = applyImageSlotSrc(editState, slot, asset.imageSrc); setEditState(nextState); setIsApplyingHistoryImage(true); try { await saveStateNow(nextState); setLocalError(null); } catch (caughtError) { setLocalError( caughtError instanceof Error ? caughtError.message : '套用历史图片失败。', ); } finally { setIsApplyingHistoryImage(false); } }; const handlePublish = async () => { if (!canSubmit || isPublishing) { setLocalError(blockers[0] ?? null); return; } setIsPublishing(true); try { const savedProfile = await saveNow(); const { item } = await publishSquareHoleWork( savedProfile?.profileId ?? profile.profileId, ); onPublished?.(item); setLocalError(null); } catch (caughtError) { setLocalError( caughtError instanceof Error ? caughtError.message : '发布方洞挑战失败。', ); } finally { setIsPublishing(false); } }; const busy = isBusy || isPublishing || isRegeneratingImages || isApplyingHistoryImage || isStartingTestRun; const displayError = error ?? localError; const activeImageSrc = activeImageSlot ? resolveImageSlotSrc(editState, activeImageSlot) : ''; return (
形状选项 setEditState((current) => ({ ...current, shapeOptions: [ ...current.shapeOptions, createShapeOption( current.shapeOptions.length, current.holeOptions, ), ], })) } tone="ghost" size="xs" className="min-h-0 gap-1.5 px-3 py-1.5 text-[11px]" > 新增
{editState.shapeOptions.map((option) => (
setActiveImageSlot({ kind: 'shape', title: `${option.label || '形状'}贴图`, assetKind: 'square_hole_shape_image', shapeOptionId: option.optionId, }) } className="relative grid h-20 w-20 shrink-0 place-items-center overflow-hidden bg-white/82 text-slate-600" aria-label={`查看${option.label || '形状'}贴图`} > } aspect="square" surface="none" className="h-full w-full rounded-none bg-transparent" fallbackClassName="tracking-normal text-slate-600" />
setEditState((current) => ({ ...current, shapeOptions: current.shapeOptions.map((entry) => entry.optionId === option.optionId ? { ...entry, label: event.target.value } : entry, ), })) } /> setEditState((current) => ({ ...current, shapeOptions: current.shapeOptions.map((entry) => entry.optionId === option.optionId ? { ...entry, targetHoleId: event.target.value } : entry, ), })) } > {editState.holeOptions.map((holeOption, holeIndex) => ( ))}
setEditState((current) => ({ ...current, shapeOptions: current.shapeOptions.filter( (entry) => entry.optionId !== option.optionId, ), })) } label="删除形状选项" icon={} className="p-2 text-slate-500 hover:bg-white/72 disabled:opacity-40" />
setEditState((current) => ({ ...current, shapeOptions: current.shapeOptions.map((entry) => entry.optionId === option.optionId ? { ...entry, imagePrompt: event.target.value } : entry, ), })) } />
))}
洞口选项 setEditState((current) => ({ ...current, holeOptions: [ ...current.holeOptions, createHoleOption(current.holeOptions.length), ], })) } tone="ghost" size="xs" className="min-h-0 gap-1.5 px-3 py-1.5 text-[11px]" > 新增
{editState.holeOptions.map((option) => (
setActiveImageSlot({ kind: 'hole', title: `${option.label || '洞口'}贴图`, assetKind: 'square_hole_hole_image', holeOptionId: option.holeId, }) } className="relative grid h-20 w-20 shrink-0 place-items-center overflow-hidden bg-white/82 text-slate-600" aria-label={`查看${option.label || '洞口'}贴图`} > } aspect="square" surface="none" className="h-full w-full rounded-none bg-transparent" fallbackClassName="tracking-normal text-slate-600" />
setEditState((current) => ({ ...current, holeOptions: current.holeOptions.map((entry) => entry.holeId === option.holeId ? { ...entry, label: event.target.value } : entry, ), })) } /> setEditState((current) => ({ ...current, holeOptions: current.holeOptions.map((entry) => entry.holeId === option.holeId ? { ...entry, imagePrompt: event.target.value } : entry, ), })) } />
setEditState((current) => { const nextHoleOptions = current.holeOptions.filter( (entry) => entry.holeId !== option.holeId, ); return { ...current, holeOptions: nextHoleOptions, shapeOptions: remapShapeTargetsToHoles( current.shapeOptions.map((shapeOption) => ({ ...shapeOption, targetHoleId: shapeOption.targetHoleId === option.holeId ? (nextHoleOptions[0]?.holeId ?? '') : shapeOption.targetHoleId, })), nextHoleOptions, ), }; }) } label="删除洞口选项" icon={} className="p-2 text-slate-500 hover:bg-white/72 disabled:opacity-40" />
))}
{displayError ? ( {displayError} ) : null}
{isStartingTestRun ? ( ) : ( )} 试玩 {isPublishing ? ( ) : profile.publicationStatus === 'published' ? ( ) : ( )} {profile.publicationStatus === 'published' ? '更新发布' : '发布'}
{activeImageSlot ? ( setActiveImageSlot(null)} onRegenerateImages={handleRegenerateImages} onSelectHistory={(asset) => { void handleSelectHistoryImage(activeImageSlot, asset); }} onUpload={(slot, event) => { void handleImageSlotUpload(slot, event); }} /> ) : null}
); } export default SquareHoleResultView;