new-chat.tsx 5.5 KB

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