new-chat.tsx 5.2 KB

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