new-chat.tsx 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193
  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. import { useCommand } from "../command";
  15. import { showConfirm } from "./ui-lib";
  16. import { BUILTIN_MASK_STORE } from "../masks";
  17. function getIntersectionArea(aRect: DOMRect, bRect: DOMRect) {
  18. const xmin = Math.max(aRect.x, bRect.x);
  19. const xmax = Math.min(aRect.x + aRect.width, bRect.x + bRect.width);
  20. const ymin = Math.max(aRect.y, bRect.y);
  21. const ymax = Math.min(aRect.y + aRect.height, bRect.y + bRect.height);
  22. const width = xmax - xmin;
  23. const height = ymax - ymin;
  24. const intersectionArea = width < 0 || height < 0 ? 0 : width * height;
  25. return intersectionArea;
  26. }
  27. function MaskItem(props: { mask: Mask; onClick?: () => void }) {
  28. return (
  29. <div className={styles["mask"]} onClick={props.onClick}>
  30. <MaskAvatar mask={props.mask} />
  31. <div className={styles["mask-name"] + " one-line"}>{props.mask.name}</div>
  32. </div>
  33. );
  34. }
  35. function useMaskGroup(masks: Mask[]) {
  36. const [groups, setGroups] = useState<Mask[][]>([]);
  37. useEffect(() => {
  38. const computeGroup = () => {
  39. const appBody = document.getElementById(SlotID.AppBody);
  40. if (!appBody || masks.length === 0) return;
  41. const rect = appBody.getBoundingClientRect();
  42. const maxWidth = rect.width;
  43. const maxHeight = rect.height * 0.6;
  44. const maskItemWidth = 120;
  45. const maskItemHeight = 50;
  46. const randomMask = () => masks[Math.floor(Math.random() * masks.length)];
  47. let maskIndex = 0;
  48. const nextMask = () => masks[maskIndex++ % masks.length];
  49. const rows = Math.ceil(maxHeight / maskItemHeight);
  50. const cols = Math.ceil(maxWidth / maskItemWidth);
  51. const newGroups = new Array(rows)
  52. .fill(0)
  53. .map((_, _i) =>
  54. new Array(cols)
  55. .fill(0)
  56. .map((_, j) => (j < 1 || j > cols - 2 ? randomMask() : nextMask())),
  57. );
  58. setGroups(newGroups);
  59. };
  60. computeGroup();
  61. window.addEventListener("resize", computeGroup);
  62. return () => window.removeEventListener("resize", computeGroup);
  63. // eslint-disable-next-line react-hooks/exhaustive-deps
  64. }, []);
  65. return groups;
  66. }
  67. export function NewChat() {
  68. const chatStore = useChatStore();
  69. const maskStore = useMaskStore();
  70. const masks = maskStore.getAll();
  71. const groups = useMaskGroup(masks);
  72. const navigate = useNavigate();
  73. const config = useAppConfig();
  74. const maskRef = useRef<HTMLDivElement>(null);
  75. const { state } = useLocation();
  76. const startChat = (mask?: Mask) => {
  77. setTimeout(() => {
  78. chatStore.newSession(mask);
  79. navigate(Path.Chat);
  80. }, 10);
  81. };
  82. useCommand({
  83. mask: (id) => {
  84. try {
  85. const intId = parseInt(id);
  86. const mask = maskStore.get(intId) ?? BUILTIN_MASK_STORE.get(intId);
  87. startChat(mask ?? undefined);
  88. } catch {
  89. console.error("[New Chat] failed to create chat from mask id=", id);
  90. }
  91. },
  92. });
  93. useEffect(() => {
  94. if (maskRef.current) {
  95. maskRef.current.scrollLeft =
  96. (maskRef.current.scrollWidth - maskRef.current.clientWidth) / 2;
  97. }
  98. }, [groups]);
  99. return (
  100. <div className={styles["new-chat"]}>
  101. <div className={styles["mask-header"]}>
  102. <IconButton
  103. icon={<LeftIcon />}
  104. text={Locale.NewChat.Return}
  105. onClick={() => navigate(Path.Home)}
  106. ></IconButton>
  107. {!state?.fromHome && (
  108. <IconButton
  109. text={Locale.NewChat.NotShow}
  110. onClick={async () => {
  111. if (await showConfirm(Locale.NewChat.ConfirmNoShow)) {
  112. startChat();
  113. config.update(
  114. (config) => (config.dontShowMaskSplashScreen = true),
  115. );
  116. }
  117. }}
  118. ></IconButton>
  119. )}
  120. </div>
  121. <div className={styles["mask-cards"]}>
  122. <div className={styles["mask-card"]}>
  123. <EmojiAvatar avatar="1f606" size={24} />
  124. </div>
  125. <div className={styles["mask-card"]}>
  126. <EmojiAvatar avatar="1f916" size={24} />
  127. </div>
  128. <div className={styles["mask-card"]}>
  129. <EmojiAvatar avatar="1f479" size={24} />
  130. </div>
  131. </div>
  132. <div className={styles["title"]}>{Locale.NewChat.Title}</div>
  133. <div className={styles["sub-title"]}>{Locale.NewChat.SubTitle}</div>
  134. <div className={styles["actions"]}>
  135. <IconButton
  136. text={Locale.NewChat.More}
  137. onClick={() => navigate(Path.Masks)}
  138. icon={<EyeIcon />}
  139. bordered
  140. shadow
  141. />
  142. <IconButton
  143. text={Locale.NewChat.Skip}
  144. onClick={() => startChat()}
  145. icon={<LightningIcon />}
  146. type="primary"
  147. shadow
  148. className={styles["skip"]}
  149. />
  150. </div>
  151. <div className={styles["masks"]} ref={maskRef}>
  152. {groups.map((masks, i) => (
  153. <div key={i} className={styles["mask-row"]}>
  154. {masks.map((mask, index) => (
  155. <MaskItem
  156. key={index}
  157. mask={mask}
  158. onClick={() => startChat(mask)}
  159. />
  160. ))}
  161. </div>
  162. ))}
  163. </div>
  164. </div>
  165. );
  166. }