From aeb986243c2460792ab4605d4fba223f6d8f98ab Mon Sep 17 00:00:00 2001 From: Yidadaa Date: Mon, 24 Apr 2023 01:15:44 +0800 Subject: [PATCH] feat: add mask screen --- app/components/home.module.scss | 6 +- app/components/home.tsx | 48 ++++++------- app/components/new-chat.module.scss | 100 ++++++++++++++++++++++++++++ app/components/new-chat.tsx | 92 +++++++++++++++++++++++++ app/components/sidebar.tsx | 2 +- app/constant.ts | 5 ++ app/locales/cn.ts | 5 +- app/locales/index.ts | 2 +- app/masks.ts | 3 + app/store/mask.ts | 81 ++++++++++++++++++++++ app/styles/globals.scss | 6 ++ 11 files changed, 315 insertions(+), 35 deletions(-) create mode 100644 app/components/new-chat.module.scss create mode 100644 app/components/new-chat.tsx create mode 100644 app/masks.ts create mode 100644 app/store/mask.ts diff --git a/app/components/home.module.scss b/app/components/home.module.scss index be630e1f..b8452534 100644 --- a/app/components/home.module.scss +++ b/app/components/home.module.scss @@ -272,16 +272,16 @@ } .sidebar-tail { - flex-direction: column; + flex-direction: column-reverse; align-items: center; .sidebar-actions { - flex-direction: column; + flex-direction: column-reverse; align-items: center; .sidebar-action { margin-right: 0; - margin-bottom: 15px; + margin-top: 15px; } } } diff --git a/app/components/home.tsx b/app/components/home.tsx index 851dba1a..c15b995e 100644 --- a/app/components/home.tsx +++ b/app/components/home.tsx @@ -12,7 +12,7 @@ import LoadingIcon from "../icons/three-dots.svg"; import { getCSSVar, useMobileScreen } from "../utils"; import dynamic from "next/dynamic"; -import { Path } from "../constant"; +import { Path, SlotID } from "../constant"; import { ErrorBoundary } from "./error"; import { @@ -23,6 +23,7 @@ import { } from "react-router-dom"; import { SideBar } from "./sidebar"; import { useAppConfig } from "../store/config"; +import { NewChat } from "./new-chat"; export function Loading(props: { noLogo?: boolean }) { return ( @@ -82,39 +83,29 @@ const useHasHydrated = () => { return hasHydrated; }; -function WideScreen() { +function Screen() { const config = useAppConfig(); + const location = useLocation(); + const isHome = location.pathname === Path.Home; + const isMobileScreen = useMobileScreen(); return (
- - -
- - } /> - } /> - } /> - -
-
- ); -} - -function MobileScreen() { - const location = useLocation(); - const isHome = location.pathname === Path.Home; - - return ( -
-
+
- + } /> + } /> } /> } /> @@ -124,7 +115,6 @@ function MobileScreen() { } export function Home() { - const isMobileScreen = useMobileScreen(); useSwitchTheme(); if (!useHasHydrated()) { @@ -133,7 +123,9 @@ export function Home() { return ( - {isMobileScreen ? : } + + + ); } diff --git a/app/components/new-chat.module.scss b/app/components/new-chat.module.scss new file mode 100644 index 00000000..9cd17960 --- /dev/null +++ b/app/components/new-chat.module.scss @@ -0,0 +1,100 @@ +.new-chat { + height: 100%; + width: 100%; + display: flex; + align-items: center; + justify-content: center; + flex-direction: column; + padding-top: 80px; + + .mask-cards { + display: flex; + margin-bottom: 20px; + + .mask-card { + padding: 20px 10px; + border: var(--border-in-light); + box-shadow: var(--card-shadow); + border-radius: 14px; + background-color: var(--white); + transform: scale(1); + + &:first-child { + transform: rotate(-15deg) translateY(5px); + } + + &:last-child { + transform: rotate(15deg) translateY(5px); + } + } + } + + .title { + font-size: 32px; + font-weight: bolder; + animation: slide-in ease 0.3s; + } + + .sub-title { + animation: slide-in ease 0.3s; + } + + .search-bar { + margin-top: 20px; + } + + .masks { + flex-grow: 1; + width: 100%; + overflow: hidden; + align-items: center; + padding-top: 20px; + + animation: slide-in ease 0.3s; + + .mask-row { + margin-bottom: 10px; + display: flex; + justify-content: center; + + @for $i from 1 to 10 { + &:nth-child(#{$i * 2}) { + margin-left: 50px; + } + } + + .mask { + display: flex; + align-items: center; + padding: 10px 16px; + border: var(--border-in-light); + box-shadow: var(--card-shadow); + background-color: var(--white); + border-radius: 10px; + margin-right: 10px; + width: 100px; + transform: scale(1); + cursor: pointer; + transition: all ease 0.3s; + + &:hover { + transform: translateY(-5px) scale(1.1); + z-index: 999; + border-color: var(--primary); + } + + .mask-avatar { + display: flex; + min-width: 18px; + min-height: 18px; + background-color: #eee; + border-radius: 20px; + } + + .mask-name { + margin-left: 10px; + } + } + } + } +} diff --git a/app/components/new-chat.tsx b/app/components/new-chat.tsx new file mode 100644 index 00000000..e053a7fe --- /dev/null +++ b/app/components/new-chat.tsx @@ -0,0 +1,92 @@ +import { useEffect, useRef } from "react"; +import { SlotID } from "../constant"; +import { EmojiAvatar } from "./emoji"; +import styles from "./new-chat.module.scss"; + +function getIntersectionArea(aRect: DOMRect, bRect: DOMRect) { + const xmin = Math.max(aRect.x, bRect.x); + const xmax = Math.min(aRect.x + aRect.width, bRect.x + bRect.width); + const ymin = Math.max(aRect.y, bRect.y); + const ymax = Math.min(aRect.y + aRect.height, bRect.y + bRect.height); + const width = xmax - xmin; + const height = ymax - ymin; + const intersectionArea = width < 0 || height < 0 ? 0 : width * height; + return intersectionArea; +} + +function Mask(props: { avatar: string; name: string }) { + const domRef = useRef(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 ( +
+
+ +
+
{props.name}
+
+ ); +} + +export function NewChat() { + const masks = new Array(20).fill(0).map(() => + new Array(10).fill(0).map((_, i) => ({ + avatar: "1f" + (Math.round(Math.random() * 50) + 600).toString(), + name: ["撩妹达人", "编程高手", "情感大师", "健康医生", "数码通"][ + Math.floor(Math.random() * 4) + ], + })), + ); + + return ( +
+
+
+ +
+
+ +
+
+ +
+
+ +
挑选一个面具
+
现在开始,与面具背后的思维碰撞
+ + + +
+ {masks.map((masks, i) => ( +
+ {masks.map((mask, index) => ( + + ))} +
+ ))} +
+
+ ); +} diff --git a/app/components/sidebar.tsx b/app/components/sidebar.tsx index 1e35964d..8b534192 100644 --- a/app/components/sidebar.tsx +++ b/app/components/sidebar.tsx @@ -134,7 +134,7 @@ export function SideBar(props: { className?: string }) { icon={} text={shouldNarrow ? undefined : Locale.Home.NewChat} onClick={() => { - chatStore.newSession(); + navigate(Path.NewChat); }} shadow /> diff --git a/app/constant.ts b/app/constant.ts index 43ae4cc6..60bb73bd 100644 --- a/app/constant.ts +++ b/app/constant.ts @@ -11,6 +11,11 @@ export enum Path { Home = "/", Chat = "/chat", Settings = "/settings", + NewChat = "/new-chat", +} + +export enum SlotID { + AppBody = "app-body", } export const MAX_SIDEBAR_WIDTH = 500; diff --git a/app/locales/cn.ts b/app/locales/cn.ts index 2e35cb30..0b8a467b 100644 --- a/app/locales/cn.ts +++ b/app/locales/cn.ts @@ -3,7 +3,8 @@ import { SubmitKey } from "../store/config"; const cn = { WIP: "该功能仍在开发中……", Error: { - Unauthorized: "现在是未授权状态,请点击左下角设置按钮输入访问密码。", + Unauthorized: + "现在是未授权状态,请点击左下角[设置](/#/settings)按钮输入访问密码。", }, ChatItem: { ChatItemCount: (count: number) => `${count} 条对话`, @@ -141,7 +142,7 @@ const cn = { Model: "模型 (model)", Temperature: { Title: "随机性 (temperature)", - SubTitle: "值越大,回复越随机,大于 1 的值可能会导致乱码", + SubTitle: "值越大,回复越随机", }, MaxTokens: { Title: "单次回复限制 (max_tokens)", diff --git a/app/locales/index.ts b/app/locales/index.ts index 389304f8..2ce59261 100644 --- a/app/locales/index.ts +++ b/app/locales/index.ts @@ -19,7 +19,7 @@ export const AllLangs = [ "jp", "de", ] as const; -type Lang = (typeof AllLangs)[number]; +export type Lang = (typeof AllLangs)[number]; const LANG_KEY = "lang"; diff --git a/app/masks.ts b/app/masks.ts new file mode 100644 index 00000000..213d9a47 --- /dev/null +++ b/app/masks.ts @@ -0,0 +1,3 @@ +import { Mask } from "./store/mask"; + +export const BUILT_IN_MASKS: Mask[] = []; diff --git a/app/store/mask.ts b/app/store/mask.ts new file mode 100644 index 00000000..168761cc --- /dev/null +++ b/app/store/mask.ts @@ -0,0 +1,81 @@ +import { create } from "zustand"; +import { persist } from "zustand/middleware"; +import { getLang, Lang } from "../locales"; +import { Message } from "./chat"; +import { ModelConfig, useAppConfig } from "./config"; + +export const MASK_KEY = "mask-store"; + +export type Mask = { + id: number; + avatar: string; + name: string; + context: Message[]; + config: ModelConfig; + lang: Lang; +}; + +export const DEFAULT_MASK_STATE = { + masks: {} as Record, + globalMaskId: 0, +}; + +export type MaskState = typeof DEFAULT_MASK_STATE; +type MaskStore = MaskState & { + create: (mask: Partial) => Mask; + update: (id: number, updater: (mask: Mask) => void) => void; + delete: (id: number) => void; + search: (text: string) => Mask[]; + getAll: () => Mask[]; +}; + +export const useMaskStore = create()( + persist( + (set, get) => ({ + ...DEFAULT_MASK_STATE, + + create(mask) { + set(() => ({ globalMaskId: get().globalMaskId + 1 })); + const id = get().globalMaskId; + const masks = get().masks; + masks[id] = { + id, + avatar: "1f916", + name: "", + config: useAppConfig.getState().modelConfig, + context: [], + lang: getLang(), + ...mask, + }; + + set(() => ({ masks })); + + return masks[id]; + }, + update(id, updater) { + const masks = get().masks; + const mask = masks[id]; + if (!mask) return; + const updateMask = { ...mask }; + updater(updateMask); + masks[id] = updateMask; + set(() => ({ masks })); + }, + delete(id) { + const masks = get().masks; + delete masks[id]; + set(() => ({ masks })); + }, + getAll() { + return Object.values(get().masks).sort((a, b) => a.id - b.id); + }, + search(text) { + return Object.values(get().masks); + }, + }), + { + name: MASK_KEY, + version: 2, + }, + ), +); diff --git a/app/styles/globals.scss b/app/styles/globals.scss index c5ffacbc..549f254b 100644 --- a/app/styles/globals.scss +++ b/app/styles/globals.scss @@ -336,3 +336,9 @@ pre { box-shadow: var(--card-shadow); border-radius: 10px; } + +.one-line { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +}