forked from XiaoMo/ChatGPT-Next-Web
152 lines
4.6 KiB
TypeScript
152 lines
4.6 KiB
TypeScript
import { useEffect, useRef, useState } from "react";
|
||
import { Path, SlotID } from "../constant";
|
||
import { IconButton } from "./button";
|
||
import { EmojiAvatar } from "./emoji";
|
||
import styles from "./new-chat.module.scss";
|
||
import LeftIcon from "../icons/left.svg";
|
||
import { useNavigate } from "react-router-dom";
|
||
import { createEmptyMask, Mask, useMaskStore } from "../store/mask";
|
||
import { useWindowSize } from "../utils";
|
||
import { useChatStore } from "../store";
|
||
import { MaskAvatar } from "./mask";
|
||
|
||
function getIntersectionArea(aRect: DOMRect, bRect: DOMRect) {
|
||
const xmin = Math.max(aRect.x, bRect.x);
|
||
const xmax = Math.min(aRect.x + aRect.width, bRect.x + bRect.width);
|
||
const ymin = Math.max(aRect.y, bRect.y);
|
||
const ymax = Math.min(aRect.y + aRect.height, bRect.y + bRect.height);
|
||
const width = xmax - xmin;
|
||
const height = ymax - ymin;
|
||
const intersectionArea = width < 0 || height < 0 ? 0 : width * height;
|
||
return intersectionArea;
|
||
}
|
||
|
||
function MaskItem(props: { mask: Mask; onClick?: () => void }) {
|
||
const domRef = useRef<HTMLDivElement>(null);
|
||
|
||
useEffect(() => {
|
||
const changeOpacity = () => {
|
||
const dom = domRef.current;
|
||
const parent = document.getElementById(SlotID.AppBody);
|
||
if (!parent || !dom) return;
|
||
|
||
const domRect = dom.getBoundingClientRect();
|
||
const parentRect = parent.getBoundingClientRect();
|
||
const intersectionArea = getIntersectionArea(domRect, parentRect);
|
||
const domArea = domRect.width * domRect.height;
|
||
const ratio = intersectionArea / domArea;
|
||
const opacity = ratio > 0.9 ? 1 : 0.4;
|
||
dom.style.opacity = opacity.toString();
|
||
};
|
||
|
||
setTimeout(changeOpacity, 30);
|
||
|
||
window.addEventListener("resize", changeOpacity);
|
||
|
||
return () => window.removeEventListener("resize", changeOpacity);
|
||
}, [domRef]);
|
||
|
||
return (
|
||
<div className={styles["mask"]} ref={domRef} onClick={props.onClick}>
|
||
<MaskAvatar mask={props.mask} />
|
||
<div className={styles["mask-name"] + " one-line"}>{props.mask.name}</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
function useMaskGroup(masks: Mask[]) {
|
||
const [groups, setGroups] = useState<Mask[][]>([]);
|
||
|
||
useEffect(() => {
|
||
const appBody = document.getElementById(SlotID.AppBody);
|
||
if (!appBody) return;
|
||
|
||
const rect = appBody.getBoundingClientRect();
|
||
const maxWidth = rect.width;
|
||
const maxHeight = rect.height * 0.6;
|
||
const maskItemWidth = 120;
|
||
const maskItemHeight = 50;
|
||
|
||
const randomMask = () => masks[Math.floor(Math.random() * masks.length)];
|
||
let maskIndex = 0;
|
||
const nextMask = () => masks[maskIndex++ % masks.length];
|
||
|
||
const rows = Math.ceil(maxHeight / maskItemHeight);
|
||
const cols = Math.ceil(maxWidth / maskItemWidth);
|
||
|
||
const newGroups = new Array(rows)
|
||
.fill(0)
|
||
.map((_, _i) =>
|
||
new Array(cols)
|
||
.fill(0)
|
||
.map((_, j) => (j < 1 || j > cols - 2 ? randomMask() : nextMask())),
|
||
);
|
||
|
||
setGroups(newGroups);
|
||
|
||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||
}, []);
|
||
|
||
return groups;
|
||
}
|
||
|
||
export function NewChat() {
|
||
const chatStore = useChatStore();
|
||
const maskStore = useMaskStore();
|
||
const masks = maskStore.getAll();
|
||
const groups = useMaskGroup(masks);
|
||
|
||
const navigate = useNavigate();
|
||
|
||
const startChat = (mask?: Mask) => {
|
||
chatStore.newSession(mask);
|
||
navigate(Path.Chat);
|
||
};
|
||
|
||
return (
|
||
<div className={styles["new-chat"]}>
|
||
<div className={styles["mask-header"]}>
|
||
<IconButton
|
||
icon={<LeftIcon />}
|
||
text="返回"
|
||
onClick={() => navigate(-1)}
|
||
></IconButton>
|
||
<IconButton text="跳过" onClick={() => startChat()}></IconButton>
|
||
</div>
|
||
<div className={styles["mask-cards"]}>
|
||
<div className={styles["mask-card"]}>
|
||
<EmojiAvatar avatar="1f606" size={24} />
|
||
</div>
|
||
<div className={styles["mask-card"]}>
|
||
<EmojiAvatar avatar="1f916" size={24} />
|
||
</div>
|
||
<div className={styles["mask-card"]}>
|
||
<EmojiAvatar avatar="1f479" size={24} />
|
||
</div>
|
||
</div>
|
||
|
||
<div className={styles["title"]}>挑选一个面具</div>
|
||
<div className={styles["sub-title"]}>
|
||
现在开始,与面具背后的灵魂思维碰撞
|
||
</div>
|
||
|
||
<input
|
||
className={styles["search-bar"]}
|
||
placeholder="搜索"
|
||
type="text"
|
||
onClick={() => navigate(Path.Masks)}
|
||
/>
|
||
|
||
<div className={styles["masks"]}>
|
||
{groups.map((masks, i) => (
|
||
<div key={i} className={styles["mask-row"]}>
|
||
{masks.map((mask, index) => (
|
||
<MaskItem key={index} mask={mask} onClick={startChat} />
|
||
))}
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|