You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
370 lines
21 KiB
370 lines
21 KiB
import { Message } from "@arco-design/web-react";
|
|
import { useCallback, useEffect, useRef, useState } from "react";
|
|
import { createCharacter } from "@/api/api";
|
|
import AvatarCropModal from "@/components/AvatarCropModal";
|
|
import { useCopyText } from "@/hooks/useCopyText";
|
|
|
|
import "./index.scss";
|
|
import { ROUTES } from "@/constants";
|
|
import { useNavigate } from "react-router-dom";
|
|
import { useTranslation } from "react-i18next";
|
|
|
|
const AGREEMENT_CHECKBOX_OFF_ICON = new URL("/src/assets/images/ic_checkbox_agreement_off.png", import.meta.url).href;
|
|
const AGREEMENT_CHECKBOX_ON_ICON = new URL("/src/assets/images/ic_checkbox_agreement_on.png", import.meta.url).href;
|
|
|
|
type CreateRoleModalProps = {
|
|
visible: boolean;
|
|
taskList: any[];
|
|
initialTaskId?: string | null;
|
|
onClose: () => void;
|
|
onCreated?: (taskId: string) => void;
|
|
};
|
|
|
|
function formatCreateDate(task: any): string {
|
|
const raw = task?.createTime;
|
|
if (raw == null || raw === "") return "";
|
|
const numeric = Number(raw);
|
|
const millis = Number.isFinite(numeric) ? (String(raw).length === 10 ? numeric * 1000 : numeric) : Number.NaN;
|
|
const date = Number.isFinite(millis) ? new Date(millis) : new Date(String(raw));
|
|
if (Number.isNaN(date.getTime())) return "";
|
|
const year = date.getFullYear();
|
|
const month = `${date.getMonth() + 1}`.padStart(2, "0");
|
|
const day = `${date.getDate()}`.padStart(2, "0");
|
|
const hour = `${date.getHours()}`.padStart(2, "0");
|
|
const minute = `${date.getMinutes()}`.padStart(2, "0");
|
|
return `${year}-${month}-${day} ${hour}:${minute}`;
|
|
}
|
|
|
|
export default function CreateRoleModal({ visible, taskList, initialTaskId, onClose, onCreated }: CreateRoleModalProps) {
|
|
const { t } = useTranslation();
|
|
|
|
const navigate = useNavigate();
|
|
const [activeTaskItem, setActiveTaskItem] = useState<any>(null);
|
|
const [avatar, setAvatar] = useState(activeTaskItem?.resultList?.[0]?.images?.[0] ?? "");
|
|
const [avatarOriginImage, setAvatarOriginImage] = useState(activeTaskItem?.resultList?.[0]?.images?.[0] ?? "");
|
|
const [cropModalImage, setCropModalImage] = useState("");
|
|
const [characterName, setCharacterName] = useState("");
|
|
const [description, setDescription] = useState("");
|
|
const [isPublic, setIsPublic] = useState(false);
|
|
const [agreementChecked, setAgreementChecked] = useState(false);
|
|
const [creating, setCreating] = useState(false);
|
|
const thumbListRef = useRef<HTMLDivElement>(null);
|
|
const copyText = useCopyText();
|
|
const userCreatedCharacter = activeTaskItem?.userCreatedCharacter;
|
|
const hasCreatedCharacter = Number(userCreatedCharacter?.id) > 0;
|
|
|
|
useEffect(() => {
|
|
if (visible) return;
|
|
setActiveTaskItem(null);
|
|
setAvatar("");
|
|
setAvatarOriginImage("");
|
|
setCropModalImage("");
|
|
setCharacterName("");
|
|
setDescription("");
|
|
setIsPublic(false);
|
|
setAgreementChecked(false);
|
|
setCreating(false);
|
|
}, [visible]);
|
|
|
|
useEffect(() => {
|
|
if (!visible) return;
|
|
if (initialTaskId) {
|
|
const selectedTask = taskList.find((task) => String(task.id) === String(initialTaskId));
|
|
if (selectedTask) {
|
|
setActiveTaskItem(selectedTask);
|
|
return;
|
|
}
|
|
}
|
|
setActiveTaskItem(taskList[0] ?? null);
|
|
}, [visible, initialTaskId, taskList]);
|
|
|
|
useEffect(() => {
|
|
if (!visible) return;
|
|
if (!activeTaskItem) return;
|
|
const existed = taskList.some((task) => String(task.id) === String(activeTaskItem.id));
|
|
if (!existed) {
|
|
setActiveTaskItem(taskList[0] ?? null);
|
|
return;
|
|
}
|
|
const nextTask = taskList.find((task) => String(task.id) === String(activeTaskItem.id));
|
|
if (nextTask && nextTask !== activeTaskItem) {
|
|
setActiveTaskItem(nextTask);
|
|
}
|
|
}, [visible, taskList, activeTaskItem]);
|
|
|
|
useEffect(() => {
|
|
const nextOriginImage = hasCreatedCharacter
|
|
? (userCreatedCharacter?.avatar ?? "")
|
|
: (activeTaskItem?.resultList?.[0]?.images?.[0] ?? "");
|
|
setCropModalImage("");
|
|
setAvatarOriginImage(nextOriginImage);
|
|
setAvatar(nextOriginImage);
|
|
setCharacterName(hasCreatedCharacter ? (userCreatedCharacter?.characterName ?? "") : "");
|
|
setDescription(hasCreatedCharacter ? (userCreatedCharacter?.description ?? "") : "");
|
|
}, [activeTaskItem, hasCreatedCharacter, userCreatedCharacter?.avatar, userCreatedCharacter?.characterName, userCreatedCharacter?.description]);
|
|
|
|
const openCropModal = useCallback(() => {
|
|
if (!avatarOriginImage) return;
|
|
const rawAvatarOriginImage = avatarOriginImage.split("#")[0];
|
|
setCropModalImage(`${rawAvatarOriginImage}#crop=${Date.now()}`);
|
|
}, [avatarOriginImage]);
|
|
|
|
const handleCreateCharacter = useCallback(async () => {
|
|
if (hasCreatedCharacter) {
|
|
Message.warning(t("pages.charactersCreate.createRoleModal.alreadyCreated"));
|
|
return;
|
|
}
|
|
if (!avatar) {
|
|
Message.warning(t("pages.charactersCreate.createRoleModal.uploadAvatarFirst"));
|
|
return;
|
|
}
|
|
if (!characterName.trim()) {
|
|
Message.warning(t("pages.charactersCreate.createRoleModal.inputName"));
|
|
return;
|
|
}
|
|
if (!description.trim()) {
|
|
Message.warning(t("pages.charactersCreate.createRoleModal.inputDescription"));
|
|
return;
|
|
}
|
|
|
|
if (!agreementChecked) {
|
|
Message.warning(t("pages.charactersCreate.createRoleModal.agreementRequired"));
|
|
return;
|
|
}
|
|
if (!activeTaskItem?.id) {
|
|
Message.warning(t("pages.charactersCreate.createRoleModal.taskNotFound"));
|
|
return;
|
|
}
|
|
setCreating(true);
|
|
try {
|
|
await createCharacter({
|
|
taskId: activeTaskItem.id,
|
|
avatar,
|
|
characterName,
|
|
description,
|
|
isPublic,
|
|
});
|
|
Message.success(t("pages.charactersCreate.createRoleModal.createSuccess"));
|
|
onCreated?.(String(activeTaskItem.id));
|
|
onClose();
|
|
navigate(ROUTES.CHARACTERS);
|
|
} catch {
|
|
Message.error(t("pages.charactersCreate.createRoleModal.createFailed"));
|
|
} finally {
|
|
setCreating(false);
|
|
}
|
|
}, [activeTaskItem?.id, agreementChecked, avatar, characterName, description, hasCreatedCharacter, isPublic, navigate, onClose, onCreated, t]);
|
|
|
|
|
|
|
|
if (!visible) return null;
|
|
|
|
|
|
const handleScrollThumbList = (direction: "left" | "right") => {
|
|
const container = thumbListRef.current;
|
|
if (!container) return;
|
|
const offset = direction === "left" ? -121 : 121;
|
|
container.scrollBy({ left: offset, behavior: "smooth" });
|
|
};
|
|
|
|
return (
|
|
<div className="charactersCreateRoleModal">
|
|
<div className="charactersCreateRoleModal__mask" onClick={onClose} />
|
|
<div className="charactersCreateRoleModal__container" role="dialog" aria-modal="true" aria-label={t("pages.charactersCreate.createRoleModal.dialogAria")}>
|
|
<section className="charactersCreateRoleModal__previewSection">
|
|
<div className="charactersCreateRoleModal__previewMain">
|
|
{activeTaskItem?.resultList?.[0]?.images?.[0] ? <img alt="" className="charactersCreateRoleModal__previewMainImage" src={activeTaskItem.resultList[0].images[0]} /> : null}
|
|
</div>
|
|
<div className="charactersCreateRoleModal__thumbSection">
|
|
<button
|
|
type="button"
|
|
className="charactersCreateRoleModal__thumbArrow "
|
|
onClick={() => handleScrollThumbList("left")}
|
|
style={{ transform: "rotate(180deg)" }}
|
|
>
|
|
<img alt="" src={new URL("/src/assets/images/ic_arrow_right_gray.png", import.meta.url).href} />
|
|
</button>
|
|
<div ref={thumbListRef} className="charactersCreateRoleModal__thumbList">
|
|
{taskList.map((task, index) => {
|
|
const taskId = String(task.id ?? index);
|
|
const thumbSrc = task.resultList[0]?.images?.[0] ?? "";
|
|
const isActive = String(task.id) === String(activeTaskItem?.id);
|
|
return (
|
|
<button
|
|
key={taskId}
|
|
type="button"
|
|
className={`charactersCreateRoleModal__thumbItem${isActive ? " charactersCreateRoleModal__thumbItem_active" : ""}`}
|
|
onClick={() => {
|
|
setActiveTaskItem(task);
|
|
}}
|
|
>
|
|
{thumbSrc ? <img alt="" className="charactersCreateRoleModal__thumbImage" src={thumbSrc} /> : null}
|
|
{!isActive ? <span className="charactersCreateRoleModal__thumbMask" /> : null}
|
|
</button>
|
|
);
|
|
})}
|
|
</div>
|
|
<button
|
|
type="button"
|
|
className="charactersCreateRoleModal__thumbArrow "
|
|
onClick={() => handleScrollThumbList("right")}
|
|
>
|
|
<img alt="" src={new URL("/src/assets/images/ic_arrow_right_gray.png", import.meta.url).href} />
|
|
</button>
|
|
</div>
|
|
</section>
|
|
<section className="charactersCreateRoleModal__formSection">
|
|
<button type="button" className="charactersCreateRoleModal__close" aria-label={t("button.close")} onClick={onClose}>
|
|
<img alt="" src={new URL("/src/assets/images/ic_close.png", import.meta.url).href} />
|
|
</button>
|
|
<div className="charactersCreateRoleModal__formScrollArea">
|
|
<div className="charactersCreateRoleModal__avatarAdd">
|
|
{
|
|
avatar ?
|
|
<button type="button" className="charactersCreateRoleModal__preview" >
|
|
<img alt="" className="charactersCreateRoleModal__previewImage" src={avatar} />
|
|
</button>
|
|
:
|
|
<img alt="" className="charactersCreateRoleModal__avatarAddImage" src={new URL("/src/assets/images/ic_avatar_add.png", import.meta.url).href} />
|
|
}
|
|
</div>
|
|
<div className="charactersCreateRoleModal__avatarUpdate">
|
|
<button type="button" className="charactersCreateRoleModal__avatarUpdateBtn" onClick={openCropModal} disabled={hasCreatedCharacter}>
|
|
修改头像
|
|
</button>
|
|
</div>
|
|
<div className="charactersCreateRoleModal__formBody">
|
|
<div className="charactersCreateRoleModal__fieldRow">
|
|
<label className="charactersCreateRoleModal__fieldLabel">{t("character_detail.name_label")}</label>
|
|
<input
|
|
className="charactersCreateRoleModal__nameInput"
|
|
value={characterName}
|
|
onChange={(event) => setCharacterName(event.target.value)}
|
|
placeholder={t("pages.charactersCreate.createRoleModal.required")}
|
|
disabled={hasCreatedCharacter}
|
|
/>
|
|
</div>
|
|
<div className="charactersCreateRoleModal__fieldBlock">
|
|
<label className="charactersCreateRoleModal__fieldLabel">{t("pages.charactersCreate.createRoleModal.descriptionLabel")}</label>
|
|
<textarea
|
|
className="charactersCreateRoleModal__introInput"
|
|
value={description}
|
|
onChange={(event) => setDescription(event.target.value)}
|
|
placeholder={t("pages.charactersCreate.createRoleModal.descriptionPlaceholder")}
|
|
disabled={hasCreatedCharacter}
|
|
/>
|
|
</div>
|
|
<div className="charactersCreateRoleModal__toggleRow">
|
|
<span className="charactersCreateRoleModal__fieldLabel">{t("pages.charactersCreate.createRoleModal.publicDisplay")}</span>
|
|
<button
|
|
type="button"
|
|
className={`charactersCreateRoleModal__toggle${isPublic ? " charactersCreateRoleModal__toggle_on" : ""}`}
|
|
onClick={() => setIsPublic((prev) => !prev)}
|
|
aria-label={t("pages.charactersCreate.createRoleModal.publicDisplaySwitchAria")}
|
|
aria-pressed={isPublic}
|
|
>
|
|
<span className="charactersCreateRoleModal__toggleKnob" />
|
|
<span className="charactersCreateRoleModal__toggleText">{isPublic ? "ON" : "OFF"}</span>
|
|
</button>
|
|
</div>
|
|
<div className="charactersCreateRoleModal__infoGroup">
|
|
<div className="charactersCreateRoleModal__promptBlock">
|
|
<span className="charactersCreateRoleModal__fieldLabel">{t("pages.assetDetails.promptLabel")}</span>
|
|
<p className="charactersCreateRoleModal__promptText">
|
|
{activeTaskItem?.chatPrompt ?? ""}
|
|
</p>
|
|
<img
|
|
alt=""
|
|
className="charactersCreateRoleModal__promptCopyIcon"
|
|
src={new URL("/src/assets/images/ic_copy.png", import.meta.url).href}
|
|
onClick={() => void copyText(activeTaskItem?.chatPrompt)}
|
|
/>
|
|
</div>
|
|
<div className="charactersCreateRoleModal__materialsBlock">
|
|
<span className="charactersCreateRoleModal__fieldLabel">{t("pages.assetDetails.useMaterial")}</span>
|
|
{/* <div className="charactersCreateRoleModal__materialsList">
|
|
{
|
|
activeTaskItem?.characterList?.map((character: any, index: number) => (
|
|
<div key={`${character.id}-${index}`} className="charactersCreateRoleModal__materialItem">
|
|
<img alt="" className="charactersCreateRoleModal__materialImage" src={character.threeViewImage} />
|
|
</div>
|
|
))
|
|
}
|
|
{activeTaskItem?.images?.map((image: string, index: number) => (
|
|
<div key={`${image}-${index}`} className="charactersCreateRoleModal__materialItem">
|
|
<img alt="" className="charactersCreateRoleModal__materialImage" src={image} />
|
|
</div>
|
|
))}
|
|
</div> */}
|
|
</div>
|
|
<div className="charactersCreateRoleModal__metaRow">
|
|
<span className="charactersCreateRoleModal__metaLabel">{t("pages.assetDetails.model")}</span>
|
|
<span className="charactersCreateRoleModal__metaValue">{activeTaskItem?.aiModelName ?? ""}</span>
|
|
</div>
|
|
<div className="charactersCreateRoleModal__metaRow">
|
|
<span className="charactersCreateRoleModal__metaLabel">{t("pages.assetDetails.ratio")}</span>
|
|
<span className="charactersCreateRoleModal__metaValue">{activeTaskItem?.aspectRatio ?? ""}</span>
|
|
</div>
|
|
<div className="charactersCreateRoleModal__metaRow">
|
|
<span className="charactersCreateRoleModal__metaLabel">{t("pages.assetDetails.size")}</span>
|
|
<span className="charactersCreateRoleModal__metaValue">{activeTaskItem?.width && activeTaskItem?.height ? `${activeTaskItem.width}x${activeTaskItem.height}` : ""}</span>
|
|
</div>
|
|
<div className="charactersCreateRoleModal__metaRow">
|
|
<span className="charactersCreateRoleModal__metaLabel">{t("pages.charactersCreate.createRoleModal.createDate")}</span>
|
|
<span className="charactersCreateRoleModal__metaValue">{formatCreateDate(activeTaskItem)}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div className="charactersCreateRoleModal__footer">
|
|
<div className="charactersCreateRoleModal__agreement">
|
|
<button
|
|
type="button"
|
|
className="charactersCreateRoleModal__agreementCheckbox"
|
|
aria-label={t("pages.charactersCreate.createRoleModal.agreeTermsAria")}
|
|
aria-checked={agreementChecked}
|
|
onClick={() => setAgreementChecked((prev) => !prev)}
|
|
>
|
|
<img
|
|
alt=""
|
|
className="charactersCreateRoleModal__agreementCheckboxIcon"
|
|
src={agreementChecked ? AGREEMENT_CHECKBOX_ON_ICON : AGREEMENT_CHECKBOX_OFF_ICON}
|
|
/>
|
|
</button>
|
|
<p className="charactersCreateRoleModal__agreementText">
|
|
{t("pages.charactersCreate.createRoleModal.uploadMeansAgree")}
|
|
<a href={ROUTES.LEGAL + "#user-agreement"} target="_blank" rel="noreferrer">{t("app.user_terms")}</a>
|
|
{t("auth.agreement_sub2")}
|
|
<a href={ROUTES.LEGAL + "#privacy-policy"} target="_blank" rel="noreferrer">{t("app.privacy_policy")}</a>
|
|
</p>
|
|
</div>
|
|
<button
|
|
type="button"
|
|
className="charactersCreateRoleModal__submitBtn"
|
|
onClick={() => {
|
|
if (hasCreatedCharacter) {
|
|
Message.warning(t("pages.charactersCreate.createRoleModal.alreadyCreated"));
|
|
return;
|
|
}
|
|
void handleCreateCharacter();
|
|
}}
|
|
disabled={creating}
|
|
aria-disabled={hasCreatedCharacter || creating}
|
|
style={hasCreatedCharacter ? { opacity: 0.5, cursor: "not-allowed" } : undefined}
|
|
>
|
|
{t("pages.charactersCreate.createRole")}
|
|
</button>
|
|
</div>
|
|
</section>
|
|
</div>
|
|
<AvatarCropModal
|
|
image={cropModalImage}
|
|
onUploaded={(uploadedAvatarUrl) => {
|
|
setAvatar(uploadedAvatarUrl);
|
|
setCropModalImage("");
|
|
}}
|
|
onClose={() => setCropModalImage("")}
|
|
/>
|
|
</div>
|
|
);
|
|
}
|
|
|