ChatGPT-Next-Web/app/components/new-chat.tsx

186 lines
5.5 KiB
TypeScript
Raw Normal View History

2023-04-25 18:02:46 +00:00
import { useEffect, useRef, useState } from "react";
import { Path, SlotID } from "../constant";
2023-04-24 16:49:27 +00:00
import { IconButton } from "./button";
2023-04-23 17:15:44 +00:00
import { EmojiAvatar } from "./emoji";
import styles from "./new-chat.module.scss";
2023-04-24 16:49:27 +00:00
import LeftIcon from "../icons/left.svg";
2023-05-01 19:10:13 +00:00
import LightningIcon from "../icons/lightning.svg";
import EyeIcon from "../icons/eye.svg";
import { useLocation, useNavigate } from "react-router-dom";
2023-05-01 19:10:13 +00:00
import { Mask, useMaskStore } from "../store/mask";
2023-04-26 17:16:21 +00:00
import Locale from "../locales";
import { useAppConfig, useChatStore } from "../store";
2023-04-25 18:02:46 +00:00
import { MaskAvatar } from "./mask";
2023-04-23 17:15:44 +00:00
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;
}
2023-04-25 18:02:46 +00:00
function MaskItem(props: { mask: Mask; onClick?: () => void }) {
2023-04-23 17:15:44 +00:00
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 (
2023-04-25 18:02:46 +00:00
<div className={styles["mask"]} ref={domRef} onClick={props.onClick}>
<MaskAvatar mask={props.mask} />
<div className={styles["mask-name"] + " one-line"}>{props.mask.name}</div>
2023-04-23 17:15:44 +00:00
</div>
);
}
2023-04-25 18:02:46 +00:00
function useMaskGroup(masks: Mask[]) {
const [groups, setGroups] = useState<Mask[][]>([]);
useEffect(() => {
const appBody = document.getElementById(SlotID.AppBody);
2023-04-26 17:16:21 +00:00
if (!appBody || masks.length === 0) return;
2023-04-25 18:02:46 +00:00
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;
}
2023-04-23 17:15:44 +00:00
export function NewChat() {
2023-04-25 18:02:46 +00:00
const chatStore = useChatStore();
const maskStore = useMaskStore();
2023-04-25 18:02:46 +00:00
const masks = maskStore.getAll();
const groups = useMaskGroup(masks);
2023-04-23 17:15:44 +00:00
2023-04-24 16:49:27 +00:00
const navigate = useNavigate();
const config = useAppConfig();
const { state } = useLocation();
2023-04-24 16:49:27 +00:00
2023-04-25 18:02:46 +00:00
const startChat = (mask?: Mask) => {
chatStore.newSession(mask);
navigate(Path.Chat);
};
2023-04-23 17:15:44 +00:00
return (
<div className={styles["new-chat"]}>
2023-04-24 16:49:27 +00:00
<div className={styles["mask-header"]}>
<IconButton
icon={<LeftIcon />}
2023-04-26 17:16:21 +00:00
text={Locale.NewChat.Return}
onClick={() => navigate(Path.Home)}
></IconButton>
{!state?.fromHome && (
<IconButton
text={Locale.NewChat.NotShow}
onClick={() => {
if (confirm(Locale.NewChat.ConfirmNoShow)) {
startChat();
config.update(
(config) => (config.dontShowMaskSplashScreen = true),
);
}
}}
></IconButton>
)}
2023-04-24 16:49:27 +00:00
</div>
2023-04-23 17:15:44 +00:00
<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>
2023-04-26 17:16:21 +00:00
<div className={styles["title"]}>{Locale.NewChat.Title}</div>
<div className={styles["sub-title"]}>{Locale.NewChat.SubTitle}</div>
2023-04-23 17:15:44 +00:00
<div className={styles["actions"]}>
<IconButton
text={Locale.NewChat.Skip}
onClick={() => startChat()}
2023-05-01 19:10:13 +00:00
icon={<LightningIcon />}
type="primary"
shadow
/>
2023-05-01 19:10:13 +00:00
<IconButton
className={styles["more"]}
text={Locale.NewChat.More}
onClick={() => navigate(Path.Masks)}
icon={<EyeIcon />}
bordered
shadow
/>
</div>
2023-04-23 17:15:44 +00:00
<div className={styles["masks"]}>
2023-04-25 18:02:46 +00:00
{groups.map((masks, i) => (
2023-04-23 17:15:44 +00:00
<div key={i} className={styles["mask-row"]}>
{masks.map((mask, index) => (
2023-04-26 17:16:21 +00:00
<MaskItem
key={index}
mask={mask}
onClick={() => startChat(mask)}
/>
2023-04-23 17:15:44 +00:00
))}
</div>
))}
</div>
</div>
);
}