new-chat.tsx 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185
  1. import { useEffect, useRef, useState } from "react";
  2. import { Path, SlotID } from "../constant";
  3. import { IconButton } from "./button";
  4. import { EmojiAvatar } from "./emoji";
  5. import styles from "./new-chat.module.scss";
  6. import LeftIcon from "../icons/left.svg";
  7. import LightningIcon from "../icons/lightning.svg";
  8. import EyeIcon from "../icons/eye.svg";
  9. import { useLocation, useNavigate } from "react-router-dom";
  10. import { Mask, useMaskStore } from "../store/mask";
  11. import Locale from "../locales";
  12. import { useAppConfig, useChatStore } from "../store";
  13. import { MaskAvatar } from "./mask";
  14. function getIntersectionArea(aRect: DOMRect, bRect: DOMRect) {
  15. const xmin = Math.max(aRect.x, bRect.x);
  16. const xmax = Math.min(aRect.x + aRect.width, bRect.x + bRect.width);
  17. const ymin = Math.max(aRect.y, bRect.y);
  18. const ymax = Math.min(aRect.y + aRect.height, bRect.y + bRect.height);
  19. const width = xmax - xmin;
  20. const height = ymax - ymin;
  21. const intersectionArea = width < 0 || height < 0 ? 0 : width * height;
  22. return intersectionArea;
  23. }
  24. function MaskItem(props: { mask: Mask; onClick?: () => void }) {
  25. const domRef = useRef<HTMLDivElement>(null);
  26. useEffect(() => {
  27. const changeOpacity = () => {
  28. const dom = domRef.current;
  29. const parent = document.getElementById(SlotID.AppBody);
  30. if (!parent || !dom) return;
  31. const domRect = dom.getBoundingClientRect();
  32. const parentRect = parent.getBoundingClientRect();
  33. const intersectionArea = getIntersectionArea(domRect, parentRect);
  34. const domArea = domRect.width * domRect.height;
  35. const ratio = intersectionArea / domArea;
  36. const opacity = ratio > 0.9 ? 1 : 0.4;
  37. dom.style.opacity = opacity.toString();
  38. };
  39. setTimeout(changeOpacity, 30);
  40. window.addEventListener("resize", changeOpacity);
  41. return () => window.removeEventListener("resize", changeOpacity);
  42. }, [domRef]);
  43. return (
  44. <div className={styles["mask"]} ref={domRef} onClick={props.onClick}>
  45. <MaskAvatar mask={props.mask} />
  46. <div className={styles["mask-name"] + " one-line"}>{props.mask.name}</div>
  47. </div>
  48. );
  49. }
  50. function useMaskGroup(masks: Mask[]) {
  51. const [groups, setGroups] = useState<Mask[][]>([]);
  52. useEffect(() => {
  53. const appBody = document.getElementById(SlotID.AppBody);
  54. if (!appBody || masks.length === 0) return;
  55. const rect = appBody.getBoundingClientRect();
  56. const maxWidth = rect.width;
  57. const maxHeight = rect.height * 0.6;
  58. const maskItemWidth = 120;
  59. const maskItemHeight = 50;
  60. const randomMask = () => masks[Math.floor(Math.random() * masks.length)];
  61. let maskIndex = 0;
  62. const nextMask = () => masks[maskIndex++ % masks.length];
  63. const rows = Math.ceil(maxHeight / maskItemHeight);
  64. const cols = Math.ceil(maxWidth / maskItemWidth);
  65. const newGroups = new Array(rows)
  66. .fill(0)
  67. .map((_, _i) =>
  68. new Array(cols)
  69. .fill(0)
  70. .map((_, j) => (j < 1 || j > cols - 2 ? randomMask() : nextMask())),
  71. );
  72. setGroups(newGroups);
  73. // eslint-disable-next-line react-hooks/exhaustive-deps
  74. }, []);
  75. return groups;
  76. }
  77. export function NewChat() {
  78. const chatStore = useChatStore();
  79. const maskStore = useMaskStore();
  80. const masks = maskStore.getAll();
  81. const groups = useMaskGroup(masks);
  82. const navigate = useNavigate();
  83. const config = useAppConfig();
  84. const { state } = useLocation();
  85. const startChat = (mask?: Mask) => {
  86. chatStore.newSession(mask);
  87. navigate(Path.Chat);
  88. };
  89. return (
  90. <div className={styles["new-chat"]}>
  91. <div className={styles["mask-header"]}>
  92. <IconButton
  93. icon={<LeftIcon />}
  94. text={Locale.NewChat.Return}
  95. onClick={() => navigate(Path.Home)}
  96. ></IconButton>
  97. {!state?.fromHome && (
  98. <IconButton
  99. text={Locale.NewChat.NotShow}
  100. onClick={() => {
  101. if (confirm(Locale.NewChat.ConfirmNoShow)) {
  102. startChat();
  103. config.update(
  104. (config) => (config.dontShowMaskSplashScreen = true),
  105. );
  106. }
  107. }}
  108. ></IconButton>
  109. )}
  110. </div>
  111. <div className={styles["mask-cards"]}>
  112. <div className={styles["mask-card"]}>
  113. <EmojiAvatar avatar="1f606" size={24} />
  114. </div>
  115. <div className={styles["mask-card"]}>
  116. <EmojiAvatar avatar="1f916" size={24} />
  117. </div>
  118. <div className={styles["mask-card"]}>
  119. <EmojiAvatar avatar="1f479" size={24} />
  120. </div>
  121. </div>
  122. <div className={styles["title"]}>{Locale.NewChat.Title}</div>
  123. <div className={styles["sub-title"]}>{Locale.NewChat.SubTitle}</div>
  124. <div className={styles["actions"]}>
  125. <IconButton
  126. text={Locale.NewChat.Skip}
  127. onClick={() => startChat()}
  128. icon={<LightningIcon />}
  129. type="primary"
  130. shadow
  131. />
  132. <IconButton
  133. className={styles["more"]}
  134. text={Locale.NewChat.More}
  135. onClick={() => navigate(Path.Masks)}
  136. icon={<EyeIcon />}
  137. bordered
  138. shadow
  139. />
  140. </div>
  141. <div className={styles["masks"]}>
  142. {groups.map((masks, i) => (
  143. <div key={i} className={styles["mask-row"]}>
  144. {masks.map((mask, index) => (
  145. <MaskItem
  146. key={index}
  147. mask={mask}
  148. onClick={() => startChat(mask)}
  149. />
  150. ))}
  151. </div>
  152. ))}
  153. </div>
  154. </div>
  155. );
  156. }