forked from XiaoMo/ChatGPT-Next-Web
feat: add mask crud
This commit is contained in:
parent
ffa7302571
commit
a7a8aad9bc
@ -57,7 +57,9 @@ export function ChatItem(props: {
|
|||||||
<div className={styles["chat-item-count"]}>
|
<div className={styles["chat-item-count"]}>
|
||||||
{Locale.ChatItem.ChatItemCount(props.count)}
|
{Locale.ChatItem.ChatItemCount(props.count)}
|
||||||
</div>
|
</div>
|
||||||
<div className={styles["chat-item-date"]}>{props.time}</div>
|
<div className={styles["chat-item-date"]}>
|
||||||
|
{new Date(props.time).toLocaleString()}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
@ -9,8 +9,8 @@ import ReturnIcon from "../icons/return.svg";
|
|||||||
import CopyIcon from "../icons/copy.svg";
|
import CopyIcon from "../icons/copy.svg";
|
||||||
import DownloadIcon from "../icons/download.svg";
|
import DownloadIcon from "../icons/download.svg";
|
||||||
import LoadingIcon from "../icons/three-dots.svg";
|
import LoadingIcon from "../icons/three-dots.svg";
|
||||||
import AddIcon from "../icons/add.svg";
|
import PromptIcon from "../icons/prompt.svg";
|
||||||
import DeleteIcon from "../icons/delete.svg";
|
import MaskIcon from "../icons/mask.svg";
|
||||||
import MaxIcon from "../icons/max.svg";
|
import MaxIcon from "../icons/max.svg";
|
||||||
import MinIcon from "../icons/min.svg";
|
import MinIcon from "../icons/min.svg";
|
||||||
|
|
||||||
@ -261,9 +261,11 @@ function useScrollToBottom() {
|
|||||||
export function ChatActions(props: {
|
export function ChatActions(props: {
|
||||||
showPromptModal: () => void;
|
showPromptModal: () => void;
|
||||||
scrollToBottom: () => void;
|
scrollToBottom: () => void;
|
||||||
|
showPromptHints: () => void;
|
||||||
hitBottom: boolean;
|
hitBottom: boolean;
|
||||||
}) {
|
}) {
|
||||||
const config = useAppConfig();
|
const config = useAppConfig();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
// switch themes
|
// switch themes
|
||||||
const theme = config.theme;
|
const theme = config.theme;
|
||||||
@ -318,6 +320,22 @@ export function ChatActions(props: {
|
|||||||
<DarkIcon />
|
<DarkIcon />
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
className={`${chatStyle["chat-input-action"]} clickable`}
|
||||||
|
onClick={props.showPromptHints}
|
||||||
|
>
|
||||||
|
<PromptIcon />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
className={`${chatStyle["chat-input-action"]} clickable`}
|
||||||
|
onClick={() => {
|
||||||
|
navigate(Path.Masks);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<MaskIcon />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -360,9 +378,9 @@ export function Chat() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const onPromptSelect = (prompt: Prompt) => {
|
const onPromptSelect = (prompt: Prompt) => {
|
||||||
setUserInput(prompt.content);
|
|
||||||
setPromptHints([]);
|
setPromptHints([]);
|
||||||
inputRef.current?.focus();
|
inputRef.current?.focus();
|
||||||
|
setUserInput(prompt.content);
|
||||||
};
|
};
|
||||||
|
|
||||||
// auto grow input
|
// auto grow input
|
||||||
@ -723,6 +741,10 @@ export function Chat() {
|
|||||||
showPromptModal={() => setShowPromptModal(true)}
|
showPromptModal={() => setShowPromptModal(true)}
|
||||||
scrollToBottom={scrollToBottom}
|
scrollToBottom={scrollToBottom}
|
||||||
hitBottom={hitBottom}
|
hitBottom={hitBottom}
|
||||||
|
showPromptHints={() => {
|
||||||
|
inputRef.current?.focus();
|
||||||
|
onSearch("");
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
<div className={styles["chat-input-panel-inner"]}>
|
<div className={styles["chat-input-panel-inner"]}>
|
||||||
<textarea
|
<textarea
|
||||||
@ -734,8 +756,12 @@ export function Chat() {
|
|||||||
onKeyDown={onInputKeyDown}
|
onKeyDown={onInputKeyDown}
|
||||||
onFocus={() => setAutoScroll(true)}
|
onFocus={() => setAutoScroll(true)}
|
||||||
onBlur={() => {
|
onBlur={() => {
|
||||||
|
setTimeout(() => {
|
||||||
|
if (document.activeElement !== inputRef.current) {
|
||||||
setAutoScroll(false);
|
setAutoScroll(false);
|
||||||
setTimeout(() => setPromptHints([]), 500);
|
setPromptHints([]);
|
||||||
|
}
|
||||||
|
}, 100);
|
||||||
}}
|
}}
|
||||||
autoFocus
|
autoFocus
|
||||||
rows={inputRows}
|
rows={inputRows}
|
||||||
|
@ -33,16 +33,16 @@ export function Avatar(props: { model?: ModelType; avatar?: string }) {
|
|||||||
return (
|
return (
|
||||||
<div className="no-dark">
|
<div className="no-dark">
|
||||||
{props.model?.startsWith("gpt-4") ? (
|
{props.model?.startsWith("gpt-4") ? (
|
||||||
<BlackBotIcon className="user-avtar" />
|
<BlackBotIcon className="user-avatar" />
|
||||||
) : (
|
) : (
|
||||||
<BotIcon className="user-avtar" />
|
<BotIcon className="user-avatar" />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="user-avtar">
|
<div className="user-avatar">
|
||||||
{props.avatar && <EmojiAvatar avatar={props.avatar} />}
|
{props.avatar && <EmojiAvatar avatar={props.avatar} />}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -1,3 +1,16 @@
|
|||||||
|
@import "../styles/animation.scss";
|
||||||
|
|
||||||
|
@keyframes search-in {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(5vh) scaleX(0.5);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0) scaleX(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.mask-page {
|
.mask-page {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -11,16 +24,50 @@
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
|
animation: search-in ease 0.3s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mask-item {
|
.mask-item {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 20px;
|
||||||
|
border: var(--border-in-light);
|
||||||
|
animation: slide-in ease 0.3s;
|
||||||
|
|
||||||
|
&:not(:last-child) {
|
||||||
|
border-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:first-child {
|
||||||
|
border-top-left-radius: 10px;
|
||||||
|
border-top-right-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
border-bottom-left-radius: 10px;
|
||||||
|
border-bottom-right-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mask-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
.mask-icon {
|
.mask-icon {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
border: var(--border-in-light);
|
margin-right: 10px;
|
||||||
border-radius: 10px;
|
}
|
||||||
padding: 6px;
|
|
||||||
|
.mask-title {
|
||||||
|
.mask-name {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
.mask-info {
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.mask-actions {
|
.mask-actions {
|
||||||
@ -28,6 +75,25 @@
|
|||||||
flex-wrap: nowrap;
|
flex-wrap: nowrap;
|
||||||
transition: all ease 0.3s;
|
transition: all ease 0.3s;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 600px) {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
border-radius: 10px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
box-shadow: var(--card-shadow);
|
||||||
|
|
||||||
|
&:not(:last-child) {
|
||||||
|
border-bottom: var(--border-in-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mask-actions {
|
||||||
|
width: 100%;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding-top: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -8,9 +8,15 @@ import EditIcon from "../icons/edit.svg";
|
|||||||
import AddIcon from "../icons/add.svg";
|
import AddIcon from "../icons/add.svg";
|
||||||
import CloseIcon from "../icons/close.svg";
|
import CloseIcon from "../icons/close.svg";
|
||||||
import DeleteIcon from "../icons/delete.svg";
|
import DeleteIcon from "../icons/delete.svg";
|
||||||
|
import BotIcon from "../icons/bot.svg";
|
||||||
import CopyIcon from "../icons/copy.svg";
|
import CopyIcon from "../icons/copy.svg";
|
||||||
|
|
||||||
import { DEFAULT_MASK_AVATAR, DEFAULT_MASK_ID, Mask } from "../store/mask";
|
import {
|
||||||
|
DEFAULT_MASK_AVATAR,
|
||||||
|
DEFAULT_MASK_ID,
|
||||||
|
Mask,
|
||||||
|
useMaskStore,
|
||||||
|
} from "../store/mask";
|
||||||
import {
|
import {
|
||||||
Message,
|
Message,
|
||||||
ModelConfig,
|
ModelConfig,
|
||||||
@ -18,7 +24,7 @@ import {
|
|||||||
useAppConfig,
|
useAppConfig,
|
||||||
useChatStore,
|
useChatStore,
|
||||||
} from "../store";
|
} from "../store";
|
||||||
import { Input, List, ListItem, Modal, Popover } from "./ui-lib";
|
import { Input, List, ListItem, Modal, Popover, showToast } from "./ui-lib";
|
||||||
import { Avatar, AvatarPicker, EmojiAvatar } from "./emoji";
|
import { Avatar, AvatarPicker, EmojiAvatar } from "./emoji";
|
||||||
import Locale from "../locales";
|
import Locale from "../locales";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
@ -28,6 +34,15 @@ import { useState } from "react";
|
|||||||
import { copyToClipboard } from "../utils";
|
import { copyToClipboard } from "../utils";
|
||||||
import { Updater } from "../api/openai/typing";
|
import { Updater } from "../api/openai/typing";
|
||||||
import { ModelConfigList } from "./model-config";
|
import { ModelConfigList } from "./model-config";
|
||||||
|
import { Path } from "../constant";
|
||||||
|
|
||||||
|
export function MaskAvatar(props: { mask: Mask }) {
|
||||||
|
return props.mask.avatar !== DEFAULT_MASK_AVATAR ? (
|
||||||
|
<Avatar avatar={props.mask.avatar} />
|
||||||
|
) : (
|
||||||
|
<Avatar model={props.mask.modelConfig.model} />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function MaskConfig(props: {
|
export function MaskConfig(props: {
|
||||||
mask: Mask;
|
mask: Mask;
|
||||||
@ -71,11 +86,7 @@ export function MaskConfig(props: {
|
|||||||
onClick={() => setShowPicker(true)}
|
onClick={() => setShowPicker(true)}
|
||||||
style={{ cursor: "pointer" }}
|
style={{ cursor: "pointer" }}
|
||||||
>
|
>
|
||||||
{props.mask.avatar !== DEFAULT_MASK_AVATAR ? (
|
<MaskAvatar mask={props.mask} />
|
||||||
<Avatar avatar={props.mask.avatar} />
|
|
||||||
) : (
|
|
||||||
<Avatar model={props.mask.modelConfig.model} />
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</Popover>
|
</Popover>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
@ -182,18 +193,15 @@ export function ContextPrompts(props: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function MaskPage() {
|
export function MaskPage() {
|
||||||
const config = useAppConfig();
|
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const masks: Mask[] = new Array(10).fill(0).map((m, i) => ({
|
|
||||||
id: i,
|
const maskStore = useMaskStore();
|
||||||
avatar: "1f606",
|
const chatStore = useChatStore();
|
||||||
name: "预设角色 " + i.toString(),
|
const masks = maskStore.getAll();
|
||||||
context: [
|
|
||||||
{ role: "assistant", content: "你好,有什么可以帮忙的吗", date: "" },
|
const [editingMaskId, setEditingMaskId] = useState<number | undefined>();
|
||||||
],
|
const editingMask = maskStore.get(editingMaskId);
|
||||||
modelConfig: config.modelConfig,
|
const closeMaskModal = () => setEditingMaskId(undefined);
|
||||||
lang: "cn",
|
|
||||||
}));
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ErrorBoundary>
|
<ErrorBoundary>
|
||||||
@ -201,12 +209,18 @@ export function MaskPage() {
|
|||||||
<div className="window-header">
|
<div className="window-header">
|
||||||
<div className="window-header-title">
|
<div className="window-header-title">
|
||||||
<div className="window-header-main-title">预设角色面具</div>
|
<div className="window-header-main-title">预设角色面具</div>
|
||||||
<div className="window-header-submai-title">编辑预设角色定义</div>
|
<div className="window-header-submai-title">
|
||||||
|
共有{masks.length} 个预设角色定义
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="window-actions">
|
<div className="window-actions">
|
||||||
<div className="window-action-button">
|
<div className="window-action-button">
|
||||||
<IconButton icon={<AddIcon />} bordered />
|
<IconButton
|
||||||
|
icon={<AddIcon />}
|
||||||
|
bordered
|
||||||
|
onClick={() => maskStore.create()}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="window-action-button">
|
<div className="window-action-button">
|
||||||
<IconButton icon={<DownloadIcon />} bordered />
|
<IconButton icon={<DownloadIcon />} bordered />
|
||||||
@ -225,34 +239,68 @@ export function MaskPage() {
|
|||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
className={styles["search-bar"]}
|
className={styles["search-bar"]}
|
||||||
placeholder="搜索面具"
|
placeholder="搜索"
|
||||||
|
autoFocus
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<List>
|
<div>
|
||||||
{masks.map((m) => (
|
{masks.map((m) => (
|
||||||
<ListItem
|
<div className={styles["mask-item"]} key={m.id}>
|
||||||
title={m.name}
|
<div className={styles["mask-header"]}>
|
||||||
key={m.id}
|
<div className={styles["mask-icon"]}>
|
||||||
subTitle={`包含 ${m.context.length} 条预设对话 / ${
|
<MaskAvatar mask={m} />
|
||||||
|
</div>
|
||||||
|
<div className={styles["mask-title"]}>
|
||||||
|
<div className={styles["mask-name"]}>{m.name}</div>
|
||||||
|
<div className={styles["mask-info"] + " one-line"}>
|
||||||
|
{`包含 ${m.context.length} 条预设对话 / ${
|
||||||
Locale.Settings.Lang.Options[m.lang]
|
Locale.Settings.Lang.Options[m.lang]
|
||||||
} / ${m.modelConfig.model}`}
|
} / ${m.modelConfig.model}`}
|
||||||
icon={
|
|
||||||
<div className={styles["mask-icon"]}>
|
|
||||||
<EmojiAvatar avatar={m.avatar} size={20} />
|
|
||||||
</div>
|
</div>
|
||||||
}
|
</div>
|
||||||
className={styles["mask-item"]}
|
</div>
|
||||||
>
|
|
||||||
<div className={styles["mask-actions"]}>
|
<div className={styles["mask-actions"]}>
|
||||||
<IconButton icon={<AddIcon />} text="对话" />
|
<IconButton
|
||||||
<IconButton icon={<EditIcon />} text="编辑" />
|
icon={<AddIcon />}
|
||||||
<IconButton icon={<DeleteIcon />} text="删除" />
|
text="对话"
|
||||||
|
onClick={() => {
|
||||||
|
chatStore.newSession(m);
|
||||||
|
navigate(Path.Chat);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<IconButton
|
||||||
|
icon={<EditIcon />}
|
||||||
|
text="编辑"
|
||||||
|
onClick={() => setEditingMaskId(m.id)}
|
||||||
|
/>
|
||||||
|
<IconButton
|
||||||
|
icon={<DeleteIcon />}
|
||||||
|
text="删除"
|
||||||
|
onClick={() => {
|
||||||
|
if (confirm("确认删除?")) {
|
||||||
|
maskStore.delete(m.id);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</ListItem>
|
|
||||||
))}
|
))}
|
||||||
</List>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{editingMask && (
|
||||||
|
<div className="modal-mask">
|
||||||
|
<Modal title="编辑预设面具" onClose={closeMaskModal}>
|
||||||
|
<MaskConfig
|
||||||
|
mask={editingMask!}
|
||||||
|
updateMask={(updater) =>
|
||||||
|
maskStore.update(editingMaskId!, updater)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Modal>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -81,13 +81,13 @@
|
|||||||
.mask {
|
.mask {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 10px 16px;
|
padding: 10px 14px;
|
||||||
border: var(--border-in-light);
|
border: var(--border-in-light);
|
||||||
box-shadow: var(--card-shadow);
|
box-shadow: var(--card-shadow);
|
||||||
background-color: var(--white);
|
background-color: var(--white);
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
margin-right: 10px;
|
margin-right: 10px;
|
||||||
width: 100px;
|
max-width: 8em;
|
||||||
transform: scale(1);
|
transform: scale(1);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all ease 0.3s;
|
transition: all ease 0.3s;
|
||||||
@ -98,16 +98,9 @@
|
|||||||
border-color: var(--primary);
|
border-color: var(--primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.mask-avatar {
|
|
||||||
display: flex;
|
|
||||||
min-width: 18px;
|
|
||||||
min-height: 18px;
|
|
||||||
background-color: #eee;
|
|
||||||
border-radius: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mask-name {
|
.mask-name {
|
||||||
margin-left: 10px;
|
margin-left: 10px;
|
||||||
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,10 +1,14 @@
|
|||||||
import { useEffect, useRef } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
import { SlotID } from "../constant";
|
import { Path, SlotID } from "../constant";
|
||||||
import { IconButton } from "./button";
|
import { IconButton } from "./button";
|
||||||
import { EmojiAvatar } from "./emoji";
|
import { EmojiAvatar } from "./emoji";
|
||||||
import styles from "./new-chat.module.scss";
|
import styles from "./new-chat.module.scss";
|
||||||
import LeftIcon from "../icons/left.svg";
|
import LeftIcon from "../icons/left.svg";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
|
import { createEmptyMask, Mask, useMaskStore } from "../store/mask";
|
||||||
|
import { useWindowSize } from "../utils";
|
||||||
|
import { useChatStore } from "../store";
|
||||||
|
import { MaskAvatar } from "./mask";
|
||||||
|
|
||||||
function getIntersectionArea(aRect: DOMRect, bRect: DOMRect) {
|
function getIntersectionArea(aRect: DOMRect, bRect: DOMRect) {
|
||||||
const xmin = Math.max(aRect.x, bRect.x);
|
const xmin = Math.max(aRect.x, bRect.x);
|
||||||
@ -17,7 +21,7 @@ function getIntersectionArea(aRect: DOMRect, bRect: DOMRect) {
|
|||||||
return intersectionArea;
|
return intersectionArea;
|
||||||
}
|
}
|
||||||
|
|
||||||
function Mask(props: { avatar: string; name: string }) {
|
function MaskItem(props: { mask: Mask; onClick?: () => void }) {
|
||||||
const domRef = useRef<HTMLDivElement>(null);
|
const domRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -43,27 +47,62 @@ function Mask(props: { avatar: string; name: string }) {
|
|||||||
}, [domRef]);
|
}, [domRef]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles["mask"]} ref={domRef}>
|
<div className={styles["mask"]} ref={domRef} onClick={props.onClick}>
|
||||||
<div className={styles["mask-avatar"]}>
|
<MaskAvatar mask={props.mask} />
|
||||||
<EmojiAvatar avatar={props.avatar} />
|
<div className={styles["mask-name"] + " one-line"}>{props.mask.name}</div>
|
||||||
</div>
|
|
||||||
<div className={styles["mask-name"] + " one-line"}>{props.name}</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function NewChat() {
|
function useMaskGroup(masks: Mask[]) {
|
||||||
const masks = new Array(20).fill(0).map(() =>
|
const [groups, setGroups] = useState<Mask[][]>([]);
|
||||||
new Array(10).fill(0).map((_, i) => ({
|
|
||||||
avatar: "1f" + (Math.round(Math.random() * 50) + 600).toString(),
|
useEffect(() => {
|
||||||
name: ["撩妹达人", "编程高手", "情感大师", "健康医生", "数码通"][
|
const appBody = document.getElementById(SlotID.AppBody);
|
||||||
Math.floor(Math.random() * 4)
|
if (!appBody) return;
|
||||||
],
|
|
||||||
})),
|
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 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())),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
setGroups(newGroups);
|
||||||
|
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return groups;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function NewChat() {
|
||||||
|
const chatStore = useChatStore();
|
||||||
|
const maskStore = useMaskStore();
|
||||||
|
const masks = maskStore.getAll();
|
||||||
|
const groups = useMaskGroup(masks);
|
||||||
|
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const startChat = (mask?: Mask) => {
|
||||||
|
chatStore.newSession(mask);
|
||||||
|
navigate(Path.Chat);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles["new-chat"]}>
|
<div className={styles["new-chat"]}>
|
||||||
<div className={styles["mask-header"]}>
|
<div className={styles["mask-header"]}>
|
||||||
@ -72,7 +111,7 @@ export function NewChat() {
|
|||||||
text="返回"
|
text="返回"
|
||||||
onClick={() => navigate(-1)}
|
onClick={() => navigate(-1)}
|
||||||
></IconButton>
|
></IconButton>
|
||||||
<IconButton text="跳过"></IconButton>
|
<IconButton text="跳过" onClick={() => startChat()}></IconButton>
|
||||||
</div>
|
</div>
|
||||||
<div className={styles["mask-cards"]}>
|
<div className={styles["mask-cards"]}>
|
||||||
<div className={styles["mask-card"]}>
|
<div className={styles["mask-card"]}>
|
||||||
@ -91,13 +130,18 @@ export function NewChat() {
|
|||||||
现在开始,与面具背后的灵魂思维碰撞
|
现在开始,与面具背后的灵魂思维碰撞
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<input className={styles["search-bar"]} placeholder="搜索" type="text" />
|
<input
|
||||||
|
className={styles["search-bar"]}
|
||||||
|
placeholder="搜索"
|
||||||
|
type="text"
|
||||||
|
onClick={() => navigate(Path.Masks)}
|
||||||
|
/>
|
||||||
|
|
||||||
<div className={styles["masks"]}>
|
<div className={styles["masks"]}>
|
||||||
{masks.map((masks, i) => (
|
{groups.map((masks, i) => (
|
||||||
<div key={i} className={styles["mask-row"]}>
|
<div key={i} className={styles["mask-row"]}>
|
||||||
{masks.map((mask, index) => (
|
{masks.map((mask, index) => (
|
||||||
<Mask key={index} {...mask} />
|
<MaskItem key={index} mask={mask} onClick={startChat} />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
@ -1 +1 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="16" height="16" viewBox="0 0 16 16" fill="none"><defs><rect id="path_0" x="0" y="0" width="16" height="16" /></defs><g opacity="1" transform="translate(0 0) rotate(0 8 8)"><mask id="bg-mask-0" fill="white"><use xlink:href="#path_0"></use></mask><g mask="url(#bg-mask-0)" ><path id="路径 1" style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0" transform="translate(1.3333333333333333 1.333333333333485) rotate(0 6.666666666666666 6.666666666666666)" d="M6.67,0L4.91,1.76L1.76,1.76L1.76,4.91L0,6.67L1.76,8.42L1.76,11.58L4.91,11.58L6.67,13.33L8.42,11.58L11.58,11.58L11.58,8.42L13.33,6.67L11.58,4.91L11.58,1.76L8.42,1.76L6.67,0Z " /><path id="路径 2" style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0" transform="translate(5.666666666666666 5.44771525016904) rotate(0 2.4732087011352872 2.442809041582063)" d="M4,0.55C2.17,-0.78 0,0.55 0,1.89C1.67,1.89 3.33,2.22 3.33,4.89C4.67,4.89 5.83,1.89 4,0.55Z " /></g></g></svg>
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="16" height="16" viewBox="0 0 16 16" fill="none"><defs><rect id="path_0" x="0" y="0" width="16" height="16" /></defs><g opacity="1" transform="translate(0 0) rotate(0 8 8)"><mask id="bg-mask-0" fill="white"><use xlink:href="#path_0"></use></mask><g mask="url(#bg-mask-0)" ><path id="路径 1" style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0" transform="translate(1.3333333333333333 1.3333333333333333) rotate(0 6.666666666666666 6.666666666666666)" d="M6.67,0C2.98,0 0,2.98 0,6.67C0,10.35 2.98,13.33 6.67,13.33C10.35,13.33 13.33,10.35 13.33,6.67C13.33,6.2 13.29,5.75 13.2,5.32C12.72,7.14 11.06,8.48 9.09,8.48C6.75,8.48 4.85,6.59 4.85,4.24C4.85,2.27 6.19,0.61 8.02,0.14C7.58,0.05 7.13,0 6.67,0Z " /></g></g></svg>
|
||||||
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 852 B |
@ -1 +1 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="16" height="16" viewBox="0 0 16 16" fill="none"><defs><rect id="path_0" x="0" y="0" width="16" height="16" /></defs><g opacity="1" transform="translate(0 0) rotate(0 8 8)"><mask id="bg-mask-0" fill="white"><use xlink:href="#path_0"></use></mask><g mask="url(#bg-mask-0)" ><path id="路径 1" style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0" transform="translate(4.340166666666667 4.21550000000002) rotate(0 3.6666666666666665 3.666666666666666)" d="M0,3.67C0,5.69 1.64,7.33 3.67,7.33C5.69,7.33 7.33,5.69 7.33,3.67C7.33,1.64 5.69,0 3.67,0C1.64,0 0,1.64 0,3.67Z " /><path id="路径 2" style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0" transform="translate(12.166666666666666 12.1719333333333) rotate(0 0.4100499999999994 0.41240499999999997)" d="M0.82,0.82L0,0 " /><path id="路径 3" style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0" transform="translate(3.0068366666666666 3.0654333333332033) rotate(0 0.3411483333333332 0.34309999999999974)" d="M0.68,0.69L0,0 " /><path id="路径 4" style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0" transform="translate(8 1.2155666666667457) rotate(0 0 0.5)" d="M0,1L0,0 " /><path id="路径 5" style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0" transform="translate(13.333266666666667 8.21550000000002) rotate(0 0.6666666666666666 0)" d="M1.33,0L0,0 " /><path id="路径 6" style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0" transform="translate(12.5108 3.065499999999929) rotate(0 0.41123333333333295 0.41123333333333295)" d="M0,0.82L0.82,0 " /><path id="路径 7" fill-rule="evenodd" style="fill:#333333" transform="translate(5.673499999999999 5.5488333333332776) rotate(0 1.1666666666666665 2.333333333333333)" opacity="1" d="M2.33,0C1.04,0 0,1.04 0,2.33C0,3.62 1.04,4.67 2.33,4.67L2.33,0Z " /><path id="路径 8" style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0" transform="translate(1.3333966666666666 7.8821666666667625) rotate(0 0.6666666666666666 0)" d="M0,0L1.33,0 " /><path id="路径 9" style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0" transform="translate(3.348133333333333 12.3125) rotate(0 0.3421333333333335 0.3421266666666665)" d="M0,0.68L0.68,0 " /><path id="路径 10" style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0" transform="translate(8 13.548763333333454) rotate(0 0 0.6666666666666666)" d="M0,1.33L0,0 " /></g></g></svg>
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="16" height="16" viewBox="0 0 16 16" fill="none"><defs><rect id="path_0" x="0" y="0" width="16" height="16" /></defs><g opacity="1" transform="translate(0 0) rotate(0 8 8)"><mask id="bg-mask-0" fill="white"><use xlink:href="#path_0"></use></mask><g mask="url(#bg-mask-0)" ><path id="路径 1" style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0" transform="translate(3.6666666666666665 3.6666666666666665) rotate(0 4.333333333333333 4.333333333333333)" d="M8.67,4.33C8.67,1.94 6.73,0 4.33,0C1.94,0 0,1.94 0,4.33C0,6.73 1.94,8.67 4.33,8.67C6.73,8.67 8.67,6.73 8.67,4.33Z " /><path id="路径 2" fill-rule="evenodd" style="fill:#333333" transform="translate(7.166666666666666 0.3333333333333333) rotate(0 0.8333333333333333 0.8333333333333333)" opacity="1" d="M1.67,0.83C1.67,0.37 1.29,0 0.83,0C0.37,0 0,0.37 0,0.83C0,1.29 0.37,1.67 0.83,1.67C1.29,1.67 1.67,1.29 1.67,0.83Z " /><path id="路径 3" fill-rule="evenodd" style="fill:#333333" transform="translate(12 2.333333333333333) rotate(0 0.8333333333333333 0.8333333333333333)" opacity="1" d="M1.67,0.83C1.67,0.37 1.29,0 0.83,0C0.37,0 0,0.37 0,0.83C0,1.29 0.37,1.67 0.83,1.67C1.29,1.67 1.67,1.29 1.67,0.83Z " /><path id="路径 4" fill-rule="evenodd" style="fill:#333333" transform="translate(14 7.166666666666666) rotate(0 0.8333333333333333 0.8333333333333333)" opacity="1" d="M1.67,0.83C1.67,0.37 1.29,0 0.83,0C0.37,0 0,0.37 0,0.83C0,1.29 0.37,1.67 0.83,1.67C1.29,1.67 1.67,1.29 1.67,0.83Z " /><path id="路径 5" fill-rule="evenodd" style="fill:#333333" transform="translate(12 12) rotate(0 0.8333333333333333 0.8333333333333333)" opacity="1" d="M1.67,0.83C1.67,0.37 1.29,0 0.83,0C0.37,0 0,0.37 0,0.83C0,1.29 0.37,1.67 0.83,1.67C1.29,1.67 1.67,1.29 1.67,0.83Z " /><path id="路径 6" fill-rule="evenodd" style="fill:#333333" transform="translate(7.166666666666666 14) rotate(0 0.8333333333333333 0.8333333333333333)" opacity="1" d="M1.67,0.83C1.67,0.37 1.29,0 0.83,0C0.37,0 0,0.37 0,0.83C0,1.29 0.37,1.67 0.83,1.67C1.29,1.67 1.67,1.29 1.67,0.83Z " /><path id="路径 7" fill-rule="evenodd" style="fill:#333333" transform="translate(2.333333333333333 12) rotate(0 0.8333333333333333 0.8333333333333333)" opacity="1" d="M1.67,0.83C1.67,0.37 1.29,0 0.83,0C0.37,0 0,0.37 0,0.83C0,1.29 0.37,1.67 0.83,1.67C1.29,1.67 1.67,1.29 1.67,0.83Z " /><path id="路径 8" fill-rule="evenodd" style="fill:#333333" transform="translate(0.3333333333333333 7.166666666666666) rotate(0 0.8333333333333333 0.8333333333333333)" opacity="1" d="M1.67,0.83C1.67,0.37 1.29,0 0.83,0C0.37,0 0,0.37 0,0.83C0,1.29 0.37,1.67 0.83,1.67C1.29,1.67 1.67,1.29 1.67,0.83Z " /><path id="路径 9" fill-rule="evenodd" style="fill:#333333" transform="translate(2.333333333333333 2.333333333333333) rotate(0 0.8333333333333333 0.8333333333333333)" opacity="1" d="M1.67,0.83C1.67,0.37 1.29,0 0.83,0C0.37,0 0,0.37 0,0.83C0,1.29 0.37,1.67 0.83,1.67C1.29,1.67 1.67,1.29 1.67,0.83Z " /></g></g></svg>
|
||||||
|
Before Width: | Height: | Size: 2.7 KiB After Width: | Height: | Size: 3.0 KiB |
1
app/icons/mask.svg
Normal file
1
app/icons/mask.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="16" height="16" viewBox="0 0 16 16" fill="none"><defs><rect id="path_0" x="0" y="0" width="16" height="16" /></defs><g opacity="1" transform="translate(0 0) rotate(0 8 8)"><mask id="bg-mask-0" fill="white"><use xlink:href="#path_0"></use></mask><g mask="url(#bg-mask-0)" ><path id="路径 1" style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0" transform="translate(2 3.333333333333333) rotate(0 6 5.666666666666666)" d="M6,0C2.69,0 0,2.54 0,5.67C0,8.8 2.69,11.33 6,11.33C9.31,11.33 12,8.8 12,5.67C12,2.54 9.31,0 6,0Z " /><path id="路径 2" style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0" transform="translate(4.64 6.715000010822796) rotate(14.999999999999998 1 1.3333333333333335)" d="M1,0C0.45,0 0,0.6 0,1.33C0,2.07 0.45,2.67 1,2.67C1.55,2.67 2,2.07 2,1.33C2,0.6 1.55,0 1,0Z " /><path id="路径 3" style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0" transform="translate(9.31 6.714999802665079) rotate(165.00000507213028 1.000000156118488 1.3333335414913166)" d="M1,0C0.45,0 0,0.6 0,1.33C0,2.07 0.45,2.67 1,2.67C1.55,2.67 2,2.07 2,1.33C2,0.6 1.55,0 1,0Z " /><path id="路径 4" style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0" transform="translate(9.666599999999999 2.492504620561264) rotate(0 2.4176172657482775 2.2535810230527007)" d="M4,4.51C5.04,3.47 5.15,1.77 4.1,0.73C3.06,-0.32 1.04,-0.2 0,0.84 " /><path id="路径 5" style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0" transform="translate(1.492667974925419 2.4926141635940393) rotate(0 2.4203326792039572 2.253609584869647)" d="M0.84,4.51C-0.2,3.47 -0.32,1.77 0.73,0.73C1.77,-0.32 3.8,-0.2 4.84,0.84 " /><path id="路径 6" style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0" transform="translate(6.5 11.67) rotate(0 1.6666666666666665 0.33333029691911636)" d="M0,0C0.17,0.43 0.73,1.09 1.67,0.29C2.6,1.09 3.17,0.43 3.33,0 " /></g></g></svg>
|
After Width: | Height: | Size: 2.1 KiB |
1
app/icons/prompt.svg
Normal file
1
app/icons/prompt.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="16" height="16" viewBox="0 0 16 16" fill="none"><defs><rect id="path_0" x="0" y="0" width="16" height="16" /></defs><g opacity="1" transform="translate(0 0) rotate(0 8 8)"><mask id="bg-mask-0" fill="white"><use xlink:href="#path_0"></use></mask><g mask="url(#bg-mask-0)" ><path id="分组 1" style="stroke:#333333; stroke-width:1.3; stroke-opacity:1; stroke-dasharray:0 0" transform="translate(5.333333333333333 1.3333333333333333) rotate(0 4.666666666666666 4.666666666666666)" d="M1.36683 1.36683L2.77683 2.77683 M4.66667 0L4.66667 2 M4.66667 2L4.66667 0 M7.9623 1.36683L6.5523 2.77683 M6.5523 2.77683L7.9623 1.36683 M9.33333 4.66667L7.33333 4.66667 M7.33333 4.66667L9.33333 4.66667 M7.9623 7.9623L6.5523 6.5523 M6.5523 6.5523L7.9623 7.9623 M4.66667 9.33333L4.66667 7.33333 M4.66667 7.33333L4.66667 9.33333 M1.36683 7.9623L2.77683 6.5523 M2.77683 6.5523L1.36683 7.9623 M0 4.66667L2 4.66667 M2 4.66667L0 4.66667 " /><path id="路径 9" style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0" transform="translate(1.847983333333333 6.1381) rotate(0 4.006941666666666 4.006933333333333)" d="M8.01,0L0,8.01 " /></g></g></svg>
|
After Width: | Height: | Size: 1.2 KiB |
@ -48,7 +48,7 @@ export interface ChatSession {
|
|||||||
memoryPrompt: string;
|
memoryPrompt: string;
|
||||||
messages: Message[];
|
messages: Message[];
|
||||||
stat: ChatStat;
|
stat: ChatStat;
|
||||||
lastUpdate: string;
|
lastUpdate: number;
|
||||||
lastSummarizeIndex: number;
|
lastSummarizeIndex: number;
|
||||||
|
|
||||||
mask: Mask;
|
mask: Mask;
|
||||||
@ -61,8 +61,6 @@ export const BOT_HELLO: Message = createMessage({
|
|||||||
});
|
});
|
||||||
|
|
||||||
function createEmptySession(): ChatSession {
|
function createEmptySession(): ChatSession {
|
||||||
const createDate = new Date().toLocaleString();
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: Date.now(),
|
id: Date.now(),
|
||||||
topic: DEFAULT_TOPIC,
|
topic: DEFAULT_TOPIC,
|
||||||
@ -73,7 +71,7 @@ function createEmptySession(): ChatSession {
|
|||||||
wordCount: 0,
|
wordCount: 0,
|
||||||
charCount: 0,
|
charCount: 0,
|
||||||
},
|
},
|
||||||
lastUpdate: createDate,
|
lastUpdate: Date.now(),
|
||||||
lastSummarizeIndex: 0,
|
lastSummarizeIndex: 0,
|
||||||
mask: createEmptyMask(),
|
mask: createEmptyMask(),
|
||||||
};
|
};
|
||||||
@ -82,11 +80,12 @@ function createEmptySession(): ChatSession {
|
|||||||
interface ChatStore {
|
interface ChatStore {
|
||||||
sessions: ChatSession[];
|
sessions: ChatSession[];
|
||||||
currentSessionIndex: number;
|
currentSessionIndex: number;
|
||||||
|
globalId: number;
|
||||||
clearSessions: () => void;
|
clearSessions: () => void;
|
||||||
removeSession: (index: number) => void;
|
removeSession: (index: number) => void;
|
||||||
moveSession: (from: number, to: number) => void;
|
moveSession: (from: number, to: number) => void;
|
||||||
selectSession: (index: number) => void;
|
selectSession: (index: number) => void;
|
||||||
newSession: () => void;
|
newSession: (mask?: Mask) => void;
|
||||||
deleteSession: (index?: number) => void;
|
deleteSession: (index?: number) => void;
|
||||||
currentSession: () => ChatSession;
|
currentSession: () => ChatSession;
|
||||||
onNewMessage: (message: Message) => void;
|
onNewMessage: (message: Message) => void;
|
||||||
@ -117,6 +116,7 @@ export const useChatStore = create<ChatStore>()(
|
|||||||
(set, get) => ({
|
(set, get) => ({
|
||||||
sessions: [createEmptySession()],
|
sessions: [createEmptySession()],
|
||||||
currentSessionIndex: 0,
|
currentSessionIndex: 0,
|
||||||
|
globalId: 0,
|
||||||
|
|
||||||
clearSessions() {
|
clearSessions() {
|
||||||
set(() => ({
|
set(() => ({
|
||||||
@ -181,10 +181,20 @@ export const useChatStore = create<ChatStore>()(
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
newSession() {
|
newSession(mask) {
|
||||||
|
const session = createEmptySession();
|
||||||
|
|
||||||
|
set(() => ({ globalId: get().globalId + 1 }));
|
||||||
|
session.id = get().globalId;
|
||||||
|
|
||||||
|
if (mask) {
|
||||||
|
session.mask = { ...mask };
|
||||||
|
session.topic = mask.name;
|
||||||
|
}
|
||||||
|
|
||||||
set((state) => ({
|
set((state) => ({
|
||||||
currentSessionIndex: 0,
|
currentSessionIndex: 0,
|
||||||
sessions: [createEmptySession()].concat(state.sessions),
|
sessions: [session].concat(state.sessions),
|
||||||
}));
|
}));
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -231,7 +241,7 @@ export const useChatStore = create<ChatStore>()(
|
|||||||
|
|
||||||
onNewMessage(message) {
|
onNewMessage(message) {
|
||||||
get().updateCurrentSession((session) => {
|
get().updateCurrentSession((session) => {
|
||||||
session.lastUpdate = new Date().toLocaleString();
|
session.lastUpdate = Date.now();
|
||||||
});
|
});
|
||||||
get().updateStat(message);
|
get().updateStat(message);
|
||||||
get().summarizeSession();
|
get().summarizeSession();
|
||||||
|
@ -22,10 +22,11 @@ export const DEFAULT_MASK_STATE = {
|
|||||||
|
|
||||||
export type MaskState = typeof DEFAULT_MASK_STATE;
|
export type MaskState = typeof DEFAULT_MASK_STATE;
|
||||||
type MaskStore = MaskState & {
|
type MaskStore = MaskState & {
|
||||||
create: (mask: Partial<Mask>) => Mask;
|
create: (mask?: Partial<Mask>) => Mask;
|
||||||
update: (id: number, updater: (mask: Mask) => void) => void;
|
update: (id: number, updater: (mask: Mask) => void) => void;
|
||||||
delete: (id: number) => void;
|
delete: (id: number) => void;
|
||||||
search: (text: string) => Mask[];
|
search: (text: string) => Mask[];
|
||||||
|
get: (id?: number) => Mask | null;
|
||||||
getAll: () => Mask[];
|
getAll: () => Mask[];
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -37,7 +38,7 @@ export const createEmptyMask = () =>
|
|||||||
avatar: DEFAULT_MASK_AVATAR,
|
avatar: DEFAULT_MASK_AVATAR,
|
||||||
name: DEFAULT_TOPIC,
|
name: DEFAULT_TOPIC,
|
||||||
context: [],
|
context: [],
|
||||||
modelConfig: useAppConfig.getState().modelConfig,
|
modelConfig: { ...useAppConfig.getState().modelConfig },
|
||||||
lang: getLang(),
|
lang: getLang(),
|
||||||
} as Mask);
|
} as Mask);
|
||||||
|
|
||||||
@ -74,6 +75,10 @@ export const useMaskStore = create<MaskStore>()(
|
|||||||
delete masks[id];
|
delete masks[id];
|
||||||
set(() => ({ masks }));
|
set(() => ({ masks }));
|
||||||
},
|
},
|
||||||
|
|
||||||
|
get(id) {
|
||||||
|
return get().masks[id ?? 1145141919810];
|
||||||
|
},
|
||||||
getAll() {
|
getAll() {
|
||||||
return Object.values(get().masks).sort((a, b) => a.id - b.id);
|
return Object.values(get().masks).sort((a, b) => a.id - b.id);
|
||||||
},
|
},
|
||||||
|
@ -329,9 +329,11 @@ pre {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.user-avtar {
|
.user-avatar {
|
||||||
height: 30px;
|
height: 30px;
|
||||||
|
min-height: 30px;
|
||||||
width: 30px;
|
width: 30px;
|
||||||
|
min-width: 30px;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
24
app/utils.ts
24
app/utils.ts
@ -47,11 +47,18 @@ export function isIOS() {
|
|||||||
return /iphone|ipad|ipod/.test(userAgent);
|
return /iphone|ipad|ipod/.test(userAgent);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useMobileScreen() {
|
export function useWindowSize() {
|
||||||
const [isMobileScreen_, setIsMobileScreen] = useState(isMobileScreen());
|
const [size, setSize] = useState({
|
||||||
|
width: window.innerWidth,
|
||||||
|
height: window.innerHeight,
|
||||||
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const onResize = () => {
|
const onResize = () => {
|
||||||
setIsMobileScreen(isMobileScreen());
|
setSize({
|
||||||
|
width: window.innerWidth,
|
||||||
|
height: window.innerHeight,
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
window.addEventListener("resize", onResize);
|
window.addEventListener("resize", onResize);
|
||||||
@ -61,14 +68,21 @@ export function useMobileScreen() {
|
|||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return isMobileScreen_;
|
return size;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const MOBILE_MAX_WIDTH = 600;
|
||||||
|
export function useMobileScreen() {
|
||||||
|
const { width } = useWindowSize();
|
||||||
|
|
||||||
|
return width <= MOBILE_MAX_WIDTH;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isMobileScreen() {
|
export function isMobileScreen() {
|
||||||
if (typeof window === "undefined") {
|
if (typeof window === "undefined") {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return window.innerWidth <= 600;
|
return window.innerWidth <= MOBILE_MAX_WIDTH;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isFirefox() {
|
export function isFirefox() {
|
||||||
|
Loading…
Reference in New Issue
Block a user