mirror of
				https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web.git
				synced 2025-11-04 08:13:43 +08:00 
			
		
		
		
	feat: scrollable mask lists in new-chat page
This commit is contained in:
		@@ -54,13 +54,13 @@
 | 
			
		||||
 | 
			
		||||
  .actions {
 | 
			
		||||
    margin-top: 5vh;
 | 
			
		||||
    margin-bottom: 5vh;
 | 
			
		||||
    margin-bottom: 2vh;
 | 
			
		||||
    animation: slide-in ease 0.45s;
 | 
			
		||||
    display: flex;
 | 
			
		||||
    justify-content: center;
 | 
			
		||||
    font-size: 12px;
 | 
			
		||||
 | 
			
		||||
    .more {
 | 
			
		||||
      font-size: 12px;
 | 
			
		||||
    .skip {
 | 
			
		||||
      margin-left: 10px;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
@@ -68,16 +68,26 @@
 | 
			
		||||
  .masks {
 | 
			
		||||
    flex-grow: 1;
 | 
			
		||||
    width: 100%;
 | 
			
		||||
    overflow: hidden;
 | 
			
		||||
    overflow: auto;
 | 
			
		||||
    align-items: center;
 | 
			
		||||
    padding-top: 20px;
 | 
			
		||||
 | 
			
		||||
    $linear: linear-gradient(
 | 
			
		||||
      to bottom,
 | 
			
		||||
      rgba(0, 0, 0, 0),
 | 
			
		||||
      rgba(0, 0, 0, 1),
 | 
			
		||||
      rgba(0, 0, 0, 0)
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    -webkit-mask-image: $linear;
 | 
			
		||||
    mask-image: $linear;
 | 
			
		||||
 | 
			
		||||
    animation: slide-in ease 0.5s;
 | 
			
		||||
 | 
			
		||||
    .mask-row {
 | 
			
		||||
      margin-bottom: 10px;
 | 
			
		||||
      display: flex;
 | 
			
		||||
      justify-content: center;
 | 
			
		||||
      // justify-content: center;
 | 
			
		||||
      margin-bottom: 10px;
 | 
			
		||||
 | 
			
		||||
      @for $i from 1 to 10 {
 | 
			
		||||
        &:nth-child(#{$i * 2}) {
 | 
			
		||||
 
 | 
			
		||||
@@ -27,32 +27,8 @@ function getIntersectionArea(aRect: DOMRect, bRect: DOMRect) {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function MaskItem(props: { mask: Mask; onClick?: () => void }) {
 | 
			
		||||
  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 (
 | 
			
		||||
    <div className={styles["mask"]} ref={domRef} onClick={props.onClick}>
 | 
			
		||||
    <div className={styles["mask"]} onClick={props.onClick}>
 | 
			
		||||
      <MaskAvatar mask={props.mask} />
 | 
			
		||||
      <div className={styles["mask-name"] + " one-line"}>{props.mask.name}</div>
 | 
			
		||||
    </div>
 | 
			
		||||
@@ -63,32 +39,36 @@ function useMaskGroup(masks: Mask[]) {
 | 
			
		||||
  const [groups, setGroups] = useState<Mask[][]>([]);
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    const appBody = document.getElementById(SlotID.AppBody);
 | 
			
		||||
    if (!appBody || masks.length === 0) return;
 | 
			
		||||
    const computeGroup = () => {
 | 
			
		||||
      const appBody = document.getElementById(SlotID.AppBody);
 | 
			
		||||
      if (!appBody || masks.length === 0) return;
 | 
			
		||||
 | 
			
		||||
    const rect = appBody.getBoundingClientRect();
 | 
			
		||||
    const maxWidth = rect.width;
 | 
			
		||||
    const maxHeight = rect.height * 0.6;
 | 
			
		||||
    const maskItemWidth = 120;
 | 
			
		||||
    const maskItemHeight = 50;
 | 
			
		||||
      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 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 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())),
 | 
			
		||||
      );
 | 
			
		||||
      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);
 | 
			
		||||
      setGroups(newGroups);
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    window.addEventListener("resize", computeGroup);
 | 
			
		||||
    return () => window.removeEventListener("resize", computeGroup);
 | 
			
		||||
    // eslint-disable-next-line react-hooks/exhaustive-deps
 | 
			
		||||
  }, []);
 | 
			
		||||
 | 
			
		||||
@@ -105,6 +85,8 @@ export function NewChat() {
 | 
			
		||||
  const navigate = useNavigate();
 | 
			
		||||
  const config = useAppConfig();
 | 
			
		||||
 | 
			
		||||
  const maskRef = useRef<HTMLDivElement>(null);
 | 
			
		||||
 | 
			
		||||
  const { state } = useLocation();
 | 
			
		||||
 | 
			
		||||
  const startChat = (mask?: Mask) => {
 | 
			
		||||
@@ -123,6 +105,13 @@ export function NewChat() {
 | 
			
		||||
    },
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    if (maskRef.current) {
 | 
			
		||||
      maskRef.current.scrollLeft =
 | 
			
		||||
        (maskRef.current.scrollWidth - maskRef.current.clientWidth) / 2;
 | 
			
		||||
    }
 | 
			
		||||
  }, [groups]);
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <div className={styles["new-chat"]}>
 | 
			
		||||
      <div className={styles["mask-header"]}>
 | 
			
		||||
@@ -162,24 +151,24 @@ export function NewChat() {
 | 
			
		||||
 | 
			
		||||
      <div className={styles["actions"]}>
 | 
			
		||||
        <IconButton
 | 
			
		||||
          text={Locale.NewChat.Skip}
 | 
			
		||||
          onClick={() => startChat()}
 | 
			
		||||
          icon={<LightningIcon />}
 | 
			
		||||
          type="primary"
 | 
			
		||||
          shadow
 | 
			
		||||
        />
 | 
			
		||||
 | 
			
		||||
        <IconButton
 | 
			
		||||
          className={styles["more"]}
 | 
			
		||||
          text={Locale.NewChat.More}
 | 
			
		||||
          onClick={() => navigate(Path.Masks)}
 | 
			
		||||
          icon={<EyeIcon />}
 | 
			
		||||
          bordered
 | 
			
		||||
          shadow
 | 
			
		||||
        />
 | 
			
		||||
 | 
			
		||||
        <IconButton
 | 
			
		||||
          text={Locale.NewChat.Skip}
 | 
			
		||||
          onClick={() => startChat()}
 | 
			
		||||
          icon={<LightningIcon />}
 | 
			
		||||
          type="primary"
 | 
			
		||||
          shadow
 | 
			
		||||
          className={styles["skip"]}
 | 
			
		||||
        />
 | 
			
		||||
      </div>
 | 
			
		||||
 | 
			
		||||
      <div className={styles["masks"]}>
 | 
			
		||||
      <div className={styles["masks"]} ref={maskRef}>
 | 
			
		||||
        {groups.map((masks, i) => (
 | 
			
		||||
          <div key={i} className={styles["mask-row"]}>
 | 
			
		||||
            {masks.map((mask, index) => (
 | 
			
		||||
 
 | 
			
		||||
@@ -221,11 +221,11 @@ const en: RequiredLocaleType = {
 | 
			
		||||
  },
 | 
			
		||||
  NewChat: {
 | 
			
		||||
    Return: "Return",
 | 
			
		||||
    Skip: "Skip",
 | 
			
		||||
    Skip: "Just Start",
 | 
			
		||||
    Title: "Pick a Mask",
 | 
			
		||||
    SubTitle: "Chat with the Soul behind the Mask",
 | 
			
		||||
    More: "Find More",
 | 
			
		||||
    NotShow: "Dont Show Again",
 | 
			
		||||
    NotShow: "Never Show Again",
 | 
			
		||||
    ConfirmNoShow: "Confirm to disable?You can enable it in settings later.",
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -73,7 +73,7 @@ export const ALL_MODELS = [
 | 
			
		||||
    available: ENABLE_GPT4,
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    name: "ext-davinci-002-render-sha-mobile",
 | 
			
		||||
    name: "text-davinci-002-render-sha-mobile",
 | 
			
		||||
    available: true,
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
@@ -106,13 +106,13 @@ export const ALL_MODELS = [
 | 
			
		||||
  },
 | 
			
		||||
] as const;
 | 
			
		||||
 | 
			
		||||
export type ModelType = typeof ALL_MODELS[number]["name"];
 | 
			
		||||
export type ModelType = (typeof ALL_MODELS)[number]["name"];
 | 
			
		||||
 | 
			
		||||
export function limitNumber(
 | 
			
		||||
  x: number,
 | 
			
		||||
  min: number,
 | 
			
		||||
  max: number,
 | 
			
		||||
  defaultValue: number
 | 
			
		||||
  defaultValue: number,
 | 
			
		||||
) {
 | 
			
		||||
  if (typeof x !== "number" || isNaN(x)) {
 | 
			
		||||
    return defaultValue;
 | 
			
		||||
@@ -171,6 +171,6 @@ export const useAppConfig = create<ChatConfigStore>()(
 | 
			
		||||
 | 
			
		||||
        return state;
 | 
			
		||||
      },
 | 
			
		||||
    }
 | 
			
		||||
  )
 | 
			
		||||
    },
 | 
			
		||||
  ),
 | 
			
		||||
);
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user