Files
omniai-web/src/features/ecommerce/panels/EcommerceClonePanel.tsx
T

760 lines
32 KiB
TypeScript
Raw Normal View History

import {
CloudUploadOutlined,
CloseOutlined,
FileImageOutlined,
LoadingOutlined,
QuestionCircleOutlined,
ReloadOutlined,
SettingOutlined,
} from "@ant-design/icons";
import type { CSSProperties, ChangeEvent, DragEvent, MutableRefObject, RefObject } from "react";
import { useRef, useState } from "react";
type ProductSetOutputKey = "set" | "detail" | "model" | "video";
type CloneOutputKey = ProductSetOutputKey | "hot" | "video-outfit";
type CloneSetCountKey = "selling" | "white" | "scene";
type CloneModelPanelTab = "scene" | "model";
type CloneReferenceMode = "upload" | "link";
type CloneReplicateLevelKey = "style" | "high";
type CloneVideoQualityKey = "standard" | "high" | "ultra";
type CloneBasicSelectKey = "platform" | "market" | "language" | "ratio";
type CloneModelSelectKey = "gender" | "age" | "ethnicity" | "body";
interface CloneImageItem {
id: string;
src: string;
name: string;
}
interface CloneBasicSelectItem {
key: CloneBasicSelectKey;
label: string;
value: string;
options: string[];
onChange: (value: string) => void;
}
interface CloneModelSelectItem {
key: CloneModelSelectKey;
label: string;
value: string;
options: string[];
onChange: (value: string) => void;
}
interface CloneSetCountOption {
key: CloneSetCountKey;
title: string;
desc: string;
}
interface CloneOutputOption {
key: CloneOutputKey;
label: string;
}
interface CloneReplicateLevelOption {
key: CloneReplicateLevelKey;
title: string;
desc: string;
}
interface CloneVideoQualityOption {
key: CloneVideoQualityKey;
label: string;
desc: string;
}
interface CloneDetailModule {
id: string;
title: string;
desc: string;
}
interface EcommerceClonePanelProps {
productInputRef: RefObject<HTMLInputElement>;
cloneReferenceInputRef: RefObject<HTMLInputElement>;
productImages: CloneImageItem[];
isProductUploadDragging: boolean;
cloneOutput: CloneOutputKey;
cloneOutputOptions: CloneOutputOption[];
cloneBasicSelects: CloneBasicSelectItem[];
openCloneBasicSelect: CloneBasicSelectKey | null;
cloneReferenceMode: CloneReferenceMode;
cloneReferenceImages: CloneImageItem[];
maxCloneReferenceImages: number;
cloneReplicateLevel: CloneReplicateLevelKey;
cloneReplicateLevelOptions: CloneReplicateLevelOption[];
cloneSetCounts: Record<CloneSetCountKey, number>;
cloneSetCountOptions: CloneSetCountOption[];
cloneSetTotal: number;
minCloneSetTotal: number;
maxCloneSetTotal: number;
selectedCloneDetailModules: string[];
cloneDetailModules: CloneDetailModule[];
cloneModelPanelTab: CloneModelPanelTab;
tryOnScenes: string[];
selectedCloneModelScenes: string[];
cloneModelCustomScene: string;
cloneModelSelects: CloneModelSelectItem[];
openCloneModelSelect: CloneModelSelectKey | null;
cloneModelSelectDropUp: boolean;
cloneModelAppearance: string;
cloneVideoQuality: CloneVideoQualityKey;
cloneVideoQualityOptions: CloneVideoQualityOption[];
cloneVideoDuration: number;
cloneVideoDurationMin: number;
cloneVideoDurationMax: number;
cloneVideoDurationStyle: CSSProperties;
cloneVideoSmart: boolean;
canGenerate: boolean;
status: string;
lastFailedActionRef: MutableRefObject<(() => void) | null>;
setIsProductUploadDragging: (value: boolean) => void;
handleProductDrop: (event: DragEvent<HTMLDivElement>) => void;
removeProductImage: (id: string) => void;
handleProductUpload: (event: ChangeEvent<HTMLInputElement>) => void;
handleCloneOutputChange: (value: CloneOutputKey) => void;
setOpenCloneBasicSelect: (value: CloneBasicSelectKey | null) => void;
setCloneReferenceMode: (value: CloneReferenceMode) => void;
handleCloneReferenceUpload: (event: ChangeEvent<HTMLInputElement>) => void;
setCloneReplicateLevel: (value: CloneReplicateLevelKey) => void;
startCloneSetCountHold: (key: CloneSetCountKey, delta: -1 | 1, disabled: boolean) => void;
clearCloneSetCountHold: () => void;
toggleCloneDetailModule: (id: string) => void;
setCloneModelPanelTab: (value: CloneModelPanelTab) => void;
toggleCloneModelScene: (scene: string) => void;
setCloneModelCustomScene: (value: string) => void;
setOpenCloneModelSelect: (value: CloneModelSelectKey | null) => void;
setCloneModelSelectDropUp: (value: boolean) => void;
setCloneModelAppearance: (value: string) => void;
setCloneVideoQuality: (value: CloneVideoQualityKey) => void;
setCloneVideoDuration: (value: number) => void;
clampCloneVideoDuration: (value: number) => number;
setCloneVideoSmart: (updater: (current: boolean) => boolean) => void;
handleGenerate: () => void;
onCancelGenerate: () => void;
formatRatioDisplayValue: (value: string) => string;
setVideoOutfitFiles?: (video: File | null, ref: File | null) => void;
onStartVideoPlan?: () => void;
}
export default function EcommerceClonePanel({
productInputRef,
cloneReferenceInputRef,
productImages,
isProductUploadDragging,
cloneOutput,
cloneOutputOptions,
cloneBasicSelects,
openCloneBasicSelect,
cloneReferenceMode,
cloneReferenceImages,
maxCloneReferenceImages,
cloneReplicateLevel,
cloneReplicateLevelOptions,
cloneSetCounts,
cloneSetCountOptions,
cloneSetTotal,
minCloneSetTotal,
maxCloneSetTotal,
selectedCloneDetailModules,
cloneDetailModules,
cloneModelPanelTab,
tryOnScenes,
selectedCloneModelScenes,
cloneModelCustomScene,
cloneModelSelects,
openCloneModelSelect,
cloneModelSelectDropUp,
cloneModelAppearance,
cloneVideoQuality,
cloneVideoQualityOptions,
cloneVideoDuration,
cloneVideoDurationMin,
cloneVideoDurationMax,
cloneVideoDurationStyle,
cloneVideoSmart,
canGenerate,
status,
lastFailedActionRef,
setIsProductUploadDragging,
handleProductDrop,
removeProductImage,
handleProductUpload,
handleCloneOutputChange,
setOpenCloneBasicSelect,
setCloneReferenceMode,
handleCloneReferenceUpload,
setCloneReplicateLevel,
startCloneSetCountHold,
clearCloneSetCountHold,
toggleCloneDetailModule,
setCloneModelPanelTab,
toggleCloneModelScene,
setCloneModelCustomScene,
setOpenCloneModelSelect,
setCloneModelSelectDropUp,
setCloneModelAppearance,
setCloneVideoQuality,
setCloneVideoDuration,
clampCloneVideoDuration,
setCloneVideoSmart,
handleGenerate,
onCancelGenerate,
formatRatioDisplayValue,
setVideoOutfitFiles,
onStartVideoPlan,
}: EcommerceClonePanelProps) {
const videoOutfitVideoRef = useRef<HTMLInputElement>(null);
const videoOutfitRefRef = useRef<HTMLInputElement>(null);
const [videoOutfitVideoUrl, setVideoOutfitVideoUrl] = useState<string | null>(null);
const [videoOutfitRefUrl, setVideoOutfitRefUrl] = useState<string | null>(null);
const handleVideoOutfitVideoChange = () => {
const file = videoOutfitVideoRef.current?.files?.[0] || null;
if (file) setVideoOutfitVideoUrl(URL.createObjectURL(file));
setVideoOutfitFiles?.(file, videoOutfitRefRef.current?.files?.[0] || null);
};
const handleVideoOutfitRefChange = () => {
const file = videoOutfitRefRef.current?.files?.[0] || null;
if (file) setVideoOutfitRefUrl(URL.createObjectURL(file));
setVideoOutfitFiles?.(videoOutfitVideoRef.current?.files?.[0] || null, file);
};
return (
<>
<div className="product-clone-panel__scroll clone-ai-panel">
<header className="clone-ai-logo">
<span className="clone-ai-logo__mark">AI</span>
<strong></strong>
</header>
<section className="clone-ai-card">
<h2>
<CloudUploadOutlined />
</h2>
<div
role="button"
tabIndex={0}
className={`clone-ai-upload-zone${isProductUploadDragging ? " is-dragging" : ""}`}
onClick={() => productInputRef.current?.click()}
onKeyDown={(event) => {
if (event.target !== event.currentTarget) return;
if (event.key === "Enter" || event.key === " ") {
event.preventDefault();
productInputRef.current?.click();
}
}}
onDragEnter={(event) => {
event.preventDefault();
setIsProductUploadDragging(true);
}}
onDragOver={(event) => event.preventDefault()}
onDragLeave={() => setIsProductUploadDragging(false)}
onDrop={handleProductDrop}
>
<div className="clone-ai-upload-main">
<span className="clone-ai-upload-icon">
<FileImageOutlined />
</span>
<span className="clone-ai-upload-title"></span>
<strong>
<span aria-hidden="true">+</span>
</strong>
<span className="clone-ai-upload-hint"> 7 </span>
</div>
{productImages.length ? (
<div className="clone-ai-uploaded-files" aria-label="已上传商品原图">
{productImages.map((item) => (
<figure key={item.id} className="clone-ai-uploaded-file">
<img src={item.src} alt={item.name} />
<span className="uploaded-image-zoom" aria-hidden="true">
<img src={item.src} alt="" />
</span>
<button
type="button"
onClick={(event) => {
event.stopPropagation();
removeProductImage(item.id);
}}
aria-label={`删除${item.name}`}
>
<CloseOutlined />
</button>
</figure>
))}
</div>
) : null}
</div>
<input ref={productInputRef} type="file" accept="image/*" multiple onChange={handleProductUpload} />
</section>
<section className="clone-ai-card">
<h2>
<SettingOutlined />
</h2>
<div className="clone-ai-settings-section">
<span className="clone-ai-settings-label"></span>
<div className="clone-ai-tag-group" role="radiogroup" aria-label="生成内容">
{cloneOutputOptions.map((option) => (
<button
key={option.key}
type="button"
className={cloneOutput === option.key ? "is-active" : ""}
aria-pressed={cloneOutput === option.key}
onClick={() => handleCloneOutputChange(option.key)}
>
{option.label}
</button>
))}
</div>
</div>
<div className="clone-ai-settings-section">
<span className="clone-ai-settings-label"></span>
<div className="clone-ai-select-group">
{cloneBasicSelects.map((item) => {
const hasMultipleOptions = item.options.length > 1;
const isOpen = hasMultipleOptions && openCloneBasicSelect === item.key;
return (
<div key={item.key} className="clone-ai-basic-select" data-clone-basic-select>
<button
type="button"
className={`${isOpen ? "is-open" : ""}${hasMultipleOptions ? "" : " is-static"}`}
aria-expanded={hasMultipleOptions ? isOpen : undefined}
aria-haspopup={hasMultipleOptions ? "listbox" : undefined}
aria-controls={hasMultipleOptions ? `clone-basic-select-${item.key}` : undefined}
onClick={() => setOpenCloneBasicSelect(hasMultipleOptions ? (isOpen ? null : item.key) : null)}
>
<span>{item.label}</span>
<strong>{item.key === "ratio" ? formatRatioDisplayValue(item.value) : item.value}</strong>
{hasMultipleOptions ? <i aria-hidden="true" /> : null}
</button>
{hasMultipleOptions && isOpen ? (
<div id={`clone-basic-select-${item.key}`} className="clone-ai-basic-select__menu" role="listbox">
{item.options.map((option) => (
<button
key={option}
type="button"
className={item.value === option ? "is-active" : ""}
role="option"
aria-selected={item.value === option}
onClick={() => {
item.onChange(option);
setOpenCloneBasicSelect(null);
}}
>
{item.key === "ratio" ? formatRatioDisplayValue(option) : option}
</button>
))}
</div>
) : null}
</div>
);
})}
</div>
</div>
</section>
{cloneOutput === "hot" ? (
<section className="clone-ai-replicate-panel" aria-label="爆款图复刻设置">
<div className="clone-ai-replicate-section">
<span className="clone-ai-replicate-title"></span>
<div className="clone-ai-replicate-tabs" role="tablist" aria-label="参考内容来源">
<button
type="button"
className={cloneReferenceMode === "upload" ? "is-active" : ""}
aria-selected={cloneReferenceMode === "upload"}
onClick={() => setCloneReferenceMode("upload")}
>
</button>
<button
type="button"
className={cloneReferenceMode === "link" ? "is-active" : ""}
aria-selected={cloneReferenceMode === "link"}
onClick={() => setCloneReferenceMode("link")}
>
</button>
</div>
{cloneReferenceMode === "upload" ? (
<button type="button" className="clone-ai-replicate-upload" onClick={() => cloneReferenceInputRef.current?.click()}>
<span>
<CloudUploadOutlined />
<span className="clone-ai-replicate-upload-text"></span>
</span>
<em>{cloneReferenceImages.length ? `已选 ${cloneReferenceImages.length}/${maxCloneReferenceImages}` : `最多 ${maxCloneReferenceImages}`}</em>
{cloneReferenceImages.length ? (
<div className="clone-ai-replicate-preview" aria-hidden="true">
{cloneReferenceImages.slice(0, 4).map((item) => (
<figure key={item.id}>
<img src={item.src} alt="" />
<span className="uploaded-image-zoom">
<img src={item.src} alt="" />
</span>
</figure>
))}
{cloneReferenceImages.length > 4 ? <b>+{cloneReferenceImages.length - 4}</b> : null}
</div>
) : null}
</button>
) : (
<label className="clone-ai-replicate-link">
<input placeholder="粘贴商品图或详情页链接" />
</label>
)}
<input
ref={cloneReferenceInputRef}
type="file"
accept="image/jpeg,image/png,image/webp"
multiple
onChange={handleCloneReferenceUpload}
/>
</div>
<div className="clone-ai-replicate-section">
<span className="clone-ai-replicate-title"></span>
<div className="clone-ai-replicate-levels" role="radiogroup" aria-label="复刻程度">
{cloneReplicateLevelOptions.map((option) => (
<button
key={option.key}
type="button"
className={cloneReplicateLevel === option.key ? "is-active" : ""}
aria-pressed={cloneReplicateLevel === option.key}
onClick={() => setCloneReplicateLevel(option.key)}
>
<strong>{option.title}</strong>
<span>{option.desc}</span>
</button>
))}
</div>
</div>
</section>
) : null}
{cloneOutput === "set" ? (
<section className="clone-ai-count-panel" aria-label="套图图片数量">
<p> 1-16 </p>
<div className="clone-ai-count-list">
{cloneSetCountOptions.map((item) => {
const count = cloneSetCounts[item.key];
const decrementDisabled = count <= 0 || cloneSetTotal <= minCloneSetTotal;
const incrementDisabled = cloneSetTotal >= maxCloneSetTotal;
return (
<div key={item.key} className="clone-ai-count-row">
<div className="clone-ai-count-copy">
<strong>{item.title}</strong>
<span>{item.desc}</span>
</div>
<div className="clone-ai-count-stepper" aria-label={`${item.title}数量`}>
<button
type="button"
disabled={decrementDisabled}
onPointerDown={(event) => {
event.preventDefault();
startCloneSetCountHold(item.key, -1, decrementDisabled);
}}
onPointerUp={clearCloneSetCountHold}
onPointerLeave={clearCloneSetCountHold}
onPointerCancel={clearCloneSetCountHold}
onBlur={clearCloneSetCountHold}
aria-label={`减少${item.title}`}
>
-
</button>
<b>{count}</b>
<button
type="button"
disabled={incrementDisabled}
onPointerDown={(event) => {
event.preventDefault();
startCloneSetCountHold(item.key, 1, incrementDisabled);
}}
onPointerUp={clearCloneSetCountHold}
onPointerLeave={clearCloneSetCountHold}
onPointerCancel={clearCloneSetCountHold}
onBlur={clearCloneSetCountHold}
aria-label={`增加${item.title}`}
>
+
</button>
</div>
</div>
);
})}
</div>
</section>
) : null}
{cloneOutput === "detail" ? (
<section className="clone-ai-module-panel" aria-label="详情图包含模块">
<p>
<QuestionCircleOutlined />
</p>
<div className="clone-ai-module-list">
{cloneDetailModules.map((module) => {
const isSelected = selectedCloneDetailModules.includes(module.id);
return (
<button
key={module.id}
type="button"
className={isSelected ? "is-active" : ""}
aria-pressed={isSelected}
onClick={() => toggleCloneDetailModule(module.id)}
>
<strong>{module.title}</strong>
<span>{module.desc}</span>
</button>
);
})}
</div>
</section>
) : null}
{cloneOutput === "model" ? (
<section className="clone-ai-model-panel" aria-label="模特图设置">
<div className="clone-ai-model-tabs" role="tablist" aria-label="模特图设置类型">
<button
type="button"
className={cloneModelPanelTab === "scene" ? "is-active" : ""}
aria-selected={cloneModelPanelTab === "scene"}
onClick={() => setCloneModelPanelTab("scene")}
>
</button>
<button
type="button"
className={cloneModelPanelTab === "model" ? "is-active" : ""}
aria-selected={cloneModelPanelTab === "model"}
onClick={() => setCloneModelPanelTab("model")}
>
</button>
</div>
<div className="clone-ai-model-scroll">
{cloneModelPanelTab === "scene" ? (
<div className="clone-ai-model-scenes">
<div className="clone-ai-model-scene-grid">
{tryOnScenes.map((scene) => {
const isSelected = selectedCloneModelScenes.includes(scene);
return (
<button
key={scene}
type="button"
className={isSelected ? "is-active" : ""}
aria-pressed={isSelected}
onClick={() => toggleCloneModelScene(scene)}
>
<span aria-hidden="true" />
{scene}
</button>
);
})}
</div>
<label className="clone-ai-model-textarea">
<strong></strong>
<textarea
value={cloneModelCustomScene}
onChange={(event) => setCloneModelCustomScene(event.target.value)}
placeholder="描述你想要的场景:如秋季枫叶小径、暖色调午后阳光、模特倚靠树干..."
/>
</label>
</div>
) : (
<div className="clone-ai-model-profile">
<div className="clone-ai-model-select-grid">
{cloneModelSelects.map((item) => {
const isOpen = openCloneModelSelect === item.key;
return (
<div
key={item.key}
className={`clone-ai-model-select${isOpen ? " is-open" : ""}${
isOpen && cloneModelSelectDropUp ? " is-drop-up" : ""
}`}
data-clone-model-select
>
<span>{item.label}</span>
<button
type="button"
className={isOpen ? "is-open" : ""}
aria-expanded={isOpen}
aria-haspopup="listbox"
aria-controls={`clone-model-select-${item.key}`}
onClick={(event) => {
setOpenCloneBasicSelect(null);
if (!isOpen) {
event.currentTarget.scrollIntoView({ block: "center", inline: "nearest" });
const triggerRect = event.currentTarget.getBoundingClientRect();
const scrollRect = event.currentTarget.closest(".clone-ai-model-scroll")?.getBoundingClientRect();
const lowerBoundary = Math.min(window.innerHeight, scrollRect?.bottom ?? window.innerHeight);
const upperBoundary = Math.max(0, scrollRect?.top ?? 0);
const estimatedMenuHeight = Math.min(150, item.options.length * 36 + 12);
const belowSpace = lowerBoundary - triggerRect.bottom;
const aboveSpace = triggerRect.top - upperBoundary;
setCloneModelSelectDropUp(belowSpace < estimatedMenuHeight && aboveSpace > belowSpace);
} else {
setCloneModelSelectDropUp(false);
}
setOpenCloneModelSelect(isOpen ? null : item.key);
}}
>
<strong>{item.value}</strong>
<i aria-hidden="true" />
</button>
{isOpen ? (
<div id={`clone-model-select-${item.key}`} className="clone-ai-model-select__menu" role="listbox">
{item.options.map((option) => (
<button
key={option}
type="button"
className={item.value === option ? "is-active" : ""}
role="option"
aria-selected={item.value === option}
onClick={() => {
item.onChange(option);
setOpenCloneModelSelect(null);
setCloneModelSelectDropUp(false);
}}
>
{option}
</button>
))}
</div>
) : null}
</div>
);
})}
</div>
<label className="clone-ai-model-textarea">
<strong></strong>
<textarea
value={cloneModelAppearance}
onChange={(event) => setCloneModelAppearance(event.target.value)}
placeholder="例如:小麦色皮肤、齐刘海、眼角有泪痣..."
/>
</label>
</div>
)}
</div>
</section>
) : null}
{cloneOutput === "video" ? (
<section className="clone-ai-video-panel" aria-label="短视频设置">
<div className="clone-ai-video-section">
<span className="clone-ai-video-title"></span>
<div className="clone-ai-video-options">
{cloneVideoQualityOptions.map((option) => (
<button
key={option.key}
type="button"
className={cloneVideoQuality === option.key ? "is-active" : ""}
aria-pressed={cloneVideoQuality === option.key}
onClick={() => setCloneVideoQuality(option.key)}
>
<strong>{option.label}</strong>
<span>{option.desc}</span>
</button>
))}
</div>
</div>
<div className="clone-ai-video-section">
<div className="clone-ai-video-title-row">
<span className="clone-ai-video-title"></span>
<strong>{cloneVideoDuration}</strong>
</div>
<div className="clone-ai-duration-control" style={cloneVideoDurationStyle}>
<input
type="range"
min={cloneVideoDurationMin}
max={cloneVideoDurationMax}
step={5}
value={cloneVideoDuration}
onChange={(event) => setCloneVideoDuration(clampCloneVideoDuration(Number(event.target.value)))}
aria-label="短视频时长"
/>
<div className="clone-ai-duration-scale" aria-hidden="true">
<span>5</span>
<span>15</span>
<span>30</span>
<span>45</span>
</div>
</div>
</div>
<button
type="button"
className={`clone-ai-video-smart${cloneVideoSmart ? " is-on" : ""}`}
aria-pressed={cloneVideoSmart}
onClick={() => setCloneVideoSmart((current) => !current)}
>
<span>
<strong></strong>
<em></em>
</span>
<i aria-hidden="true" />
</button>
</section>
) : null}
{cloneOutput === "video" && onStartVideoPlan ? (
<button type="button" className="clone-ai-generate" onClick={onStartVideoPlan}>
</button>
) : null}
{cloneOutput === "video-outfit" ? (
<section className="clone-ai-video-panel" aria-label="视频换装">
<div className="clone-ai-video-section">
<span className="clone-ai-video-title"></span>
<div className="clone-ai-video-outfit-upload">
<input
ref={videoOutfitVideoRef}
type="file"
accept="video/*"
onChange={handleVideoOutfitVideoChange}
style={{ display: "none" }}
/>
<button type="button" className="clone-ai-video-outfit-upload-btn" onClick={() => videoOutfitVideoRef.current?.click()}>
{videoOutfitVideoUrl ? "重新选择视频" : "选择视频文件"}
</button>
{videoOutfitVideoUrl ? <span className="clone-ai-video-outfit-info"></span> : null}
</div>
</div>
<div className="clone-ai-video-section">
<span className="clone-ai-video-title">/</span>
<div className="clone-ai-video-outfit-upload">
<input
ref={videoOutfitRefRef}
type="file"
accept="image/*"
onChange={handleVideoOutfitRefChange}
style={{ display: "none" }}
/>
<button type="button" className="clone-ai-video-outfit-upload-btn" onClick={() => videoOutfitRefRef.current?.click()}>
{videoOutfitRefUrl ? "重新选择参考图" : "选择参考图"}
</button>
{videoOutfitRefUrl ? <span className="clone-ai-video-outfit-info"></span> : null}
</div>
</div>
</section>
) : null}
<button type="button" className="clone-ai-generate" disabled={!canGenerate || cloneOutput === "video"} onClick={status === "failed" && lastFailedActionRef.current ? lastFailedActionRef.current : handleGenerate} style={cloneOutput === "video" ? { display: "none" } : undefined}>
{status === "generating" ? <LoadingOutlined /> : status === "failed" ? <ReloadOutlined /> : null}
{status === "generating" ? "生成中..." : status === "failed" ? "重新生成" : cloneOutput === "video-outfit" ? "✦ 开始换装" : "✦ 开始生成"}
</button>
{status === "generating" && cloneOutput !== "video" ? (
<button type="button" className="clone-ai-generate clone-ai-generate--cancel" onClick={onCancelGenerate}>
{"\u53d6\u6d88\u751f\u6210"}
</button>
) : null}
</div>
</>
);
}