forked from XiaoMo/ChatGPT-Next-Web
feat: add mask screen
This commit is contained in:
parent
e654cee3c8
commit
aeb986243c
@ -272,16 +272,16 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.sidebar-tail {
|
.sidebar-tail {
|
||||||
flex-direction: column;
|
flex-direction: column-reverse;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
||||||
.sidebar-actions {
|
.sidebar-actions {
|
||||||
flex-direction: column;
|
flex-direction: column-reverse;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
||||||
.sidebar-action {
|
.sidebar-action {
|
||||||
margin-right: 0;
|
margin-right: 0;
|
||||||
margin-bottom: 15px;
|
margin-top: 15px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -12,7 +12,7 @@ import LoadingIcon from "../icons/three-dots.svg";
|
|||||||
import { getCSSVar, useMobileScreen } from "../utils";
|
import { getCSSVar, useMobileScreen } from "../utils";
|
||||||
|
|
||||||
import dynamic from "next/dynamic";
|
import dynamic from "next/dynamic";
|
||||||
import { Path } from "../constant";
|
import { Path, SlotID } from "../constant";
|
||||||
import { ErrorBoundary } from "./error";
|
import { ErrorBoundary } from "./error";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@ -23,6 +23,7 @@ import {
|
|||||||
} from "react-router-dom";
|
} from "react-router-dom";
|
||||||
import { SideBar } from "./sidebar";
|
import { SideBar } from "./sidebar";
|
||||||
import { useAppConfig } from "../store/config";
|
import { useAppConfig } from "../store/config";
|
||||||
|
import { NewChat } from "./new-chat";
|
||||||
|
|
||||||
export function Loading(props: { noLogo?: boolean }) {
|
export function Loading(props: { noLogo?: boolean }) {
|
||||||
return (
|
return (
|
||||||
@ -82,39 +83,29 @@ const useHasHydrated = () => {
|
|||||||
return hasHydrated;
|
return hasHydrated;
|
||||||
};
|
};
|
||||||
|
|
||||||
function WideScreen() {
|
function Screen() {
|
||||||
const config = useAppConfig();
|
const config = useAppConfig();
|
||||||
|
const location = useLocation();
|
||||||
|
const isHome = location.pathname === Path.Home;
|
||||||
|
const isMobileScreen = useMobileScreen();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`${
|
className={
|
||||||
config.tightBorder ? styles["tight-container"] : styles.container
|
styles.container +
|
||||||
}`}
|
` ${
|
||||||
|
config.tightBorder && !isMobileScreen
|
||||||
|
? styles["tight-container"]
|
||||||
|
: styles.container
|
||||||
|
}`
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<SideBar />
|
|
||||||
|
|
||||||
<div className={styles["window-content"]}>
|
|
||||||
<Routes>
|
|
||||||
<Route path={Path.Home} element={<Chat />} />
|
|
||||||
<Route path={Path.Chat} element={<Chat />} />
|
|
||||||
<Route path={Path.Settings} element={<Settings />} />
|
|
||||||
</Routes>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function MobileScreen() {
|
|
||||||
const location = useLocation();
|
|
||||||
const isHome = location.pathname === Path.Home;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={styles.container}>
|
|
||||||
<SideBar className={isHome ? styles["sidebar-show"] : ""} />
|
<SideBar className={isHome ? styles["sidebar-show"] : ""} />
|
||||||
|
|
||||||
<div className={styles["window-content"]}>
|
<div className={styles["window-content"]} id={SlotID.AppBody}>
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path={Path.Home} element={null} />
|
<Route path={Path.Home} element={<Chat />} />
|
||||||
|
<Route path={Path.NewChat} element={<NewChat />} />
|
||||||
<Route path={Path.Chat} element={<Chat />} />
|
<Route path={Path.Chat} element={<Chat />} />
|
||||||
<Route path={Path.Settings} element={<Settings />} />
|
<Route path={Path.Settings} element={<Settings />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
@ -124,7 +115,6 @@ function MobileScreen() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function Home() {
|
export function Home() {
|
||||||
const isMobileScreen = useMobileScreen();
|
|
||||||
useSwitchTheme();
|
useSwitchTheme();
|
||||||
|
|
||||||
if (!useHasHydrated()) {
|
if (!useHasHydrated()) {
|
||||||
@ -133,7 +123,9 @@ export function Home() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<ErrorBoundary>
|
<ErrorBoundary>
|
||||||
<Router>{isMobileScreen ? <MobileScreen /> : <WideScreen />}</Router>
|
<Router>
|
||||||
|
<Screen />
|
||||||
|
</Router>
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
100
app/components/new-chat.module.scss
Normal file
100
app/components/new-chat.module.scss
Normal file
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
92
app/components/new-chat.tsx
Normal file
92
app/components/new-chat.tsx
Normal file
@ -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<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}>
|
||||||
|
<div className={styles["mask-avatar"]}>
|
||||||
|
<EmojiAvatar avatar={props.avatar} />
|
||||||
|
</div>
|
||||||
|
<div className={styles["mask-name"] + " one-line"}>{props.name}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<div className={styles["new-chat"]}>
|
||||||
|
<div className={styles["mask-cards"]}>
|
||||||
|
<div className={styles["mask-card"]}>
|
||||||
|
<EmojiAvatar avatar="1f606" size={24} />
|
||||||
|
</div>
|
||||||
|
<div className={styles["mask-card"]}>
|
||||||
|
<EmojiAvatar avatar="1f916" size={24} />
|
||||||
|
</div>
|
||||||
|
<div className={styles["mask-card"]}>
|
||||||
|
<EmojiAvatar avatar="1f479" size={24} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles["title"]}>挑选一个面具</div>
|
||||||
|
<div className={styles["sub-title"]}>现在开始,与面具背后的思维碰撞</div>
|
||||||
|
|
||||||
|
<input className={styles["search-bar"]} placeholder="搜索" type="text" />
|
||||||
|
|
||||||
|
<div className={styles["masks"]}>
|
||||||
|
{masks.map((masks, i) => (
|
||||||
|
<div key={i} className={styles["mask-row"]}>
|
||||||
|
{masks.map((mask, index) => (
|
||||||
|
<Mask key={index} {...mask} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
@ -134,7 +134,7 @@ export function SideBar(props: { className?: string }) {
|
|||||||
icon={<AddIcon />}
|
icon={<AddIcon />}
|
||||||
text={shouldNarrow ? undefined : Locale.Home.NewChat}
|
text={shouldNarrow ? undefined : Locale.Home.NewChat}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
chatStore.newSession();
|
navigate(Path.NewChat);
|
||||||
}}
|
}}
|
||||||
shadow
|
shadow
|
||||||
/>
|
/>
|
||||||
|
@ -11,6 +11,11 @@ export enum Path {
|
|||||||
Home = "/",
|
Home = "/",
|
||||||
Chat = "/chat",
|
Chat = "/chat",
|
||||||
Settings = "/settings",
|
Settings = "/settings",
|
||||||
|
NewChat = "/new-chat",
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum SlotID {
|
||||||
|
AppBody = "app-body",
|
||||||
}
|
}
|
||||||
|
|
||||||
export const MAX_SIDEBAR_WIDTH = 500;
|
export const MAX_SIDEBAR_WIDTH = 500;
|
||||||
|
@ -3,7 +3,8 @@ import { SubmitKey } from "../store/config";
|
|||||||
const cn = {
|
const cn = {
|
||||||
WIP: "该功能仍在开发中……",
|
WIP: "该功能仍在开发中……",
|
||||||
Error: {
|
Error: {
|
||||||
Unauthorized: "现在是未授权状态,请点击左下角设置按钮输入访问密码。",
|
Unauthorized:
|
||||||
|
"现在是未授权状态,请点击左下角[设置](/#/settings)按钮输入访问密码。",
|
||||||
},
|
},
|
||||||
ChatItem: {
|
ChatItem: {
|
||||||
ChatItemCount: (count: number) => `${count} 条对话`,
|
ChatItemCount: (count: number) => `${count} 条对话`,
|
||||||
@ -141,7 +142,7 @@ const cn = {
|
|||||||
Model: "模型 (model)",
|
Model: "模型 (model)",
|
||||||
Temperature: {
|
Temperature: {
|
||||||
Title: "随机性 (temperature)",
|
Title: "随机性 (temperature)",
|
||||||
SubTitle: "值越大,回复越随机,大于 1 的值可能会导致乱码",
|
SubTitle: "值越大,回复越随机",
|
||||||
},
|
},
|
||||||
MaxTokens: {
|
MaxTokens: {
|
||||||
Title: "单次回复限制 (max_tokens)",
|
Title: "单次回复限制 (max_tokens)",
|
||||||
|
@ -19,7 +19,7 @@ export const AllLangs = [
|
|||||||
"jp",
|
"jp",
|
||||||
"de",
|
"de",
|
||||||
] as const;
|
] as const;
|
||||||
type Lang = (typeof AllLangs)[number];
|
export type Lang = (typeof AllLangs)[number];
|
||||||
|
|
||||||
const LANG_KEY = "lang";
|
const LANG_KEY = "lang";
|
||||||
|
|
||||||
|
3
app/masks.ts
Normal file
3
app/masks.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
import { Mask } from "./store/mask";
|
||||||
|
|
||||||
|
export const BUILT_IN_MASKS: Mask[] = [];
|
81
app/store/mask.ts
Normal file
81
app/store/mask.ts
Normal file
@ -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<number, Mask>,
|
||||||
|
globalMaskId: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
export type MaskState = typeof DEFAULT_MASK_STATE;
|
||||||
|
type MaskStore = MaskState & {
|
||||||
|
create: (mask: Partial<Mask>) => Mask;
|
||||||
|
update: (id: number, updater: (mask: Mask) => void) => void;
|
||||||
|
delete: (id: number) => void;
|
||||||
|
search: (text: string) => Mask[];
|
||||||
|
getAll: () => Mask[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useMaskStore = create<MaskStore>()(
|
||||||
|
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,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
@ -336,3 +336,9 @@ pre {
|
|||||||
box-shadow: var(--card-shadow);
|
box-shadow: var(--card-shadow);
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.one-line {
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user