forked from XiaoMo/ChatGPT-Next-Web
commit
ee0f847827
@ -146,6 +146,10 @@ Access passsword, separated by comma.
|
||||
|
||||
Override openai api request base url.
|
||||
|
||||
### `OPENAI_ORG_ID` (optional)
|
||||
|
||||
Specify OpenAI organization ID.
|
||||
|
||||
## Development
|
||||
|
||||
> [简体中文 > 如何进行二次开发](./README_CN.md#开发)
|
||||
|
@ -94,6 +94,10 @@ OpenAI 接口代理 URL,如果你手动配置了 openai 接口代理,请填
|
||||
|
||||
> 如果遇到 ssl 证书问题,请将 `BASE_URL` 的协议设置为 http。
|
||||
|
||||
### `OPENAI_ORG_ID` (可选)
|
||||
|
||||
指定 OpenAI 中的组织 ID。
|
||||
|
||||
## 开发
|
||||
|
||||
> 强烈不建议在本地进行开发或者部署,由于一些技术原因,很难在本地配置好 OpenAI API 代理,除非你能保证可以直连 OpenAI 服务器。
|
||||
|
@ -10,7 +10,6 @@ import {
|
||||
import { useChatStore } from "../store";
|
||||
|
||||
import Locale from "../locales";
|
||||
import { isMobileScreen } from "../utils";
|
||||
|
||||
export function ChatItem(props: {
|
||||
onClick?: () => void;
|
||||
|
@ -10,6 +10,7 @@ import CopyIcon from "../icons/copy.svg";
|
||||
import DownloadIcon from "../icons/download.svg";
|
||||
import LoadingIcon from "../icons/three-dots.svg";
|
||||
import BotIcon from "../icons/bot.svg";
|
||||
import BlackBotIcon from "../icons/black-bot.svg";
|
||||
import AddIcon from "../icons/add.svg";
|
||||
import DeleteIcon from "../icons/delete.svg";
|
||||
import MaxIcon from "../icons/max.svg";
|
||||
@ -30,15 +31,16 @@ import {
|
||||
createMessage,
|
||||
useAccessStore,
|
||||
Theme,
|
||||
ModelType,
|
||||
} from "../store";
|
||||
|
||||
import {
|
||||
copyToClipboard,
|
||||
downloadAs,
|
||||
getEmojiUrl,
|
||||
isMobileScreen,
|
||||
selectOrCopy,
|
||||
autoGrowTextArea,
|
||||
useMobileScreen,
|
||||
} from "../utils";
|
||||
|
||||
import dynamic from "next/dynamic";
|
||||
@ -64,13 +66,17 @@ const Emoji = dynamic(async () => (await import("emoji-picker-react")).Emoji, {
|
||||
loading: () => <LoadingIcon />,
|
||||
});
|
||||
|
||||
export function Avatar(props: { role: Message["role"] }) {
|
||||
export function Avatar(props: { role: Message["role"]; model?: ModelType }) {
|
||||
const config = useChatStore((state) => state.config);
|
||||
|
||||
if (props.role !== "user") {
|
||||
return (
|
||||
<div className="no-dark">
|
||||
<BotIcon className={styles["user-avtar"]} />
|
||||
{props.model?.startsWith("gpt-4") ? (
|
||||
<BlackBotIcon className={styles["user-avtar"]} />
|
||||
) : (
|
||||
<BotIcon className={styles["user-avtar"]} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -432,6 +438,7 @@ export function Chat(props: {
|
||||
const { submitKey, shouldSubmit } = useSubmitHandler();
|
||||
const { scrollRef, setAutoScroll, scrollToBottom } = useScrollToBottom();
|
||||
const [hitBottom, setHitBottom] = useState(false);
|
||||
const isMobileScreen = useMobileScreen();
|
||||
|
||||
const onChatBodyScroll = (e: HTMLElement) => {
|
||||
const isTouchBottom = e.scrollTop + e.clientHeight >= e.scrollHeight - 20;
|
||||
@ -462,7 +469,7 @@ export function Chat(props: {
|
||||
const rows = inputRef.current ? autoGrowTextArea(inputRef.current) : 1;
|
||||
const inputRows = Math.min(
|
||||
5,
|
||||
Math.max(2 + Number(!isMobileScreen()), rows),
|
||||
Math.max(2 + Number(!isMobileScreen), rows),
|
||||
);
|
||||
setInputRows(inputRows);
|
||||
},
|
||||
@ -502,7 +509,7 @@ export function Chat(props: {
|
||||
setBeforeInput(userInput);
|
||||
setUserInput("");
|
||||
setPromptHints([]);
|
||||
if (!isMobileScreen()) inputRef.current?.focus();
|
||||
if (!isMobileScreen) inputRef.current?.focus();
|
||||
setAutoScroll(true);
|
||||
};
|
||||
|
||||
@ -634,7 +641,7 @@ export function Chat(props: {
|
||||
|
||||
// Auto focus
|
||||
useEffect(() => {
|
||||
if (props.sideBarShowing && isMobileScreen()) return;
|
||||
if (props.sideBarShowing && isMobileScreen) return;
|
||||
inputRef.current?.focus();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
@ -682,7 +689,7 @@ export function Chat(props: {
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{!isMobileScreen() && (
|
||||
{!isMobileScreen && (
|
||||
<div className={styles["window-action-button"]}>
|
||||
<IconButton
|
||||
icon={chatStore.config.tightBorder ? <MinIcon /> : <MaxIcon />}
|
||||
@ -717,6 +724,11 @@ export function Chat(props: {
|
||||
>
|
||||
{messages.map((message, i) => {
|
||||
const isUser = message.role === "user";
|
||||
const showActions =
|
||||
!isUser &&
|
||||
i > 0 &&
|
||||
!(message.preview || message.content.length === 0);
|
||||
const showTyping = message.preview || message.streaming;
|
||||
|
||||
return (
|
||||
<div
|
||||
@ -727,49 +739,48 @@ export function Chat(props: {
|
||||
>
|
||||
<div className={styles["chat-message-container"]}>
|
||||
<div className={styles["chat-message-avatar"]}>
|
||||
<Avatar role={message.role} />
|
||||
<Avatar role={message.role} model={message.model} />
|
||||
</div>
|
||||
{(message.preview || message.streaming) && (
|
||||
{showTyping && (
|
||||
<div className={styles["chat-message-status"]}>
|
||||
{Locale.Chat.Typing}
|
||||
</div>
|
||||
)}
|
||||
<div className={styles["chat-message-item"]}>
|
||||
{!isUser &&
|
||||
!(message.preview || message.content.length === 0) && (
|
||||
<div className={styles["chat-message-top-actions"]}>
|
||||
{message.streaming ? (
|
||||
<div
|
||||
className={styles["chat-message-top-action"]}
|
||||
onClick={() => onUserStop(message.id ?? i)}
|
||||
>
|
||||
{Locale.Chat.Actions.Stop}
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div
|
||||
className={styles["chat-message-top-action"]}
|
||||
onClick={() => onDelete(message.id ?? i)}
|
||||
>
|
||||
{Locale.Chat.Actions.Delete}
|
||||
</div>
|
||||
<div
|
||||
className={styles["chat-message-top-action"]}
|
||||
onClick={() => onResend(message.id ?? i)}
|
||||
>
|
||||
{Locale.Chat.Actions.Retry}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{showActions && (
|
||||
<div className={styles["chat-message-top-actions"]}>
|
||||
{message.streaming ? (
|
||||
<div
|
||||
className={styles["chat-message-top-action"]}
|
||||
onClick={() => copyToClipboard(message.content)}
|
||||
onClick={() => onUserStop(message.id ?? i)}
|
||||
>
|
||||
{Locale.Chat.Actions.Copy}
|
||||
{Locale.Chat.Actions.Stop}
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div
|
||||
className={styles["chat-message-top-action"]}
|
||||
onClick={() => onDelete(message.id ?? i)}
|
||||
>
|
||||
{Locale.Chat.Actions.Delete}
|
||||
</div>
|
||||
<div
|
||||
className={styles["chat-message-top-action"]}
|
||||
onClick={() => onResend(message.id ?? i)}
|
||||
>
|
||||
{Locale.Chat.Actions.Retry}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
<div
|
||||
className={styles["chat-message-top-action"]}
|
||||
onClick={() => copyToClipboard(message.content)}
|
||||
>
|
||||
{Locale.Chat.Actions.Copy}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<Markdown
|
||||
content={message.content}
|
||||
loading={
|
||||
@ -778,7 +789,7 @@ export function Chat(props: {
|
||||
}
|
||||
onContextMenu={(e) => onRightClick(e, message)}
|
||||
onDoubleClickCapture={() => {
|
||||
if (!isMobileScreen()) return;
|
||||
if (!isMobileScreen) return;
|
||||
setUserInput(message.content);
|
||||
}}
|
||||
fontSize={fontSize}
|
||||
|
@ -17,7 +17,7 @@ import LoadingIcon from "../icons/three-dots.svg";
|
||||
import CloseIcon from "../icons/close.svg";
|
||||
|
||||
import { useChatStore } from "../store";
|
||||
import { getCSSVar, isMobileScreen } from "../utils";
|
||||
import { getCSSVar, useMobileScreen } from "../utils";
|
||||
import Locale from "../locales";
|
||||
import { Chat } from "./chat";
|
||||
|
||||
@ -103,17 +103,14 @@ function useDragSideBar() {
|
||||
window.addEventListener("mousemove", handleMouseMove.current);
|
||||
window.addEventListener("mouseup", handleMouseUp.current);
|
||||
};
|
||||
const isMobileScreen = useMobileScreen();
|
||||
|
||||
useEffect(() => {
|
||||
if (isMobileScreen()) {
|
||||
return;
|
||||
}
|
||||
|
||||
document.documentElement.style.setProperty(
|
||||
"--sidebar-width",
|
||||
`${limit(chatStore.config.sidebarWidth ?? 300)}px`,
|
||||
);
|
||||
}, [chatStore.config.sidebarWidth]);
|
||||
const sideBarWidth = isMobileScreen
|
||||
? "100vw"
|
||||
: `${limit(chatStore.config.sidebarWidth ?? 300)}px`;
|
||||
document.documentElement.style.setProperty("--sidebar-width", sideBarWidth);
|
||||
}, [chatStore.config.sidebarWidth, isMobileScreen]);
|
||||
|
||||
return {
|
||||
onDragMouseDown,
|
||||
@ -148,6 +145,7 @@ function _Home() {
|
||||
|
||||
// drag side bar
|
||||
const { onDragMouseDown } = useDragSideBar();
|
||||
const isMobileScreen = useMobileScreen();
|
||||
|
||||
useSwitchTheme();
|
||||
|
||||
@ -158,7 +156,7 @@ function _Home() {
|
||||
return (
|
||||
<div
|
||||
className={`${
|
||||
config.tightBorder && !isMobileScreen()
|
||||
config.tightBorder && !isMobileScreen
|
||||
? styles["tight-container"]
|
||||
: styles.container
|
||||
}`}
|
||||
|
28
app/icons/black-bot.svg
Normal file
28
app/icons/black-bot.svg
Normal file
@ -0,0 +1,28 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="30"
|
||||
height="30" viewBox="0 0 30 30" fill="none">
|
||||
<defs>
|
||||
<rect id="path_0" x="0" y="0" width="29.999999999999996" height="29.999999999999996" />
|
||||
<rect id="path_1" x="0" y="0" width="20.45454545454545" height="20.45454545454545" />
|
||||
</defs>
|
||||
<g opacity="1" transform="translate(0 0) rotate(0 14.999999999999998 14.999999999999998)">
|
||||
<rect fill="#E7F8FF" opacity="1"
|
||||
transform="translate(0 0) rotate(0 14.999999999999998 14.999999999999998)" x="0" y="0"
|
||||
width="29.999999999999996" height="29.999999999999996" rx="10" />
|
||||
<mask id="bg-mask-0" fill="white">
|
||||
<use xlink:href="#path_0"></use>
|
||||
</mask>
|
||||
<g mask="url(#bg-mask-0)">
|
||||
<g opacity="1"
|
||||
transform="translate(4.772727272727272 4.772727272727273) rotate(0 10.227272727272725 10.227272727272725)">
|
||||
<mask id="bg-mask-1" fill="white">
|
||||
<use xlink:href="#path_1"></use>
|
||||
</mask>
|
||||
<g mask="url(#bg-mask-1)">
|
||||
<path id="分组 1" fill-rule="evenodd" style="fill:#000000"
|
||||
transform="translate(0 0) rotate(0 10.227272727272725 10.227272727272725)" opacity="1"
|
||||
d="M19.11 8.37L19.11 8.37C19.28 7.85 19.37 7.31 19.37 6.76C19.37 5.86 19.13 4.97 18.66 4.19C17.73 2.59 16 1.6 14.13 1.6C13.76 1.6 13.4 1.64 13.04 1.71C12.06 0.62 10.65 0 9.17 0L9.14 0L9.13 0C6.86 0 4.86 1.44 4.16 3.57C2.7 3.86 1.44 4.76 0.71 6.04C0.24 6.83 0 7.72 0 8.63C0 9.9 0.48 11.14 1.35 12.08C1.17 12.6 1.08 13.15 1.08 13.69C1.08 14.6 1.33 15.49 1.79 16.27C2.92 18.21 5.2 19.21 7.42 18.74C8.4 19.83 9.8 20.45 11.28 20.45L11.31 20.45L11.33 20.45C13.59 20.45 15.6 19.01 16.3 16.88C17.76 16.59 19.01 15.69 19.75 14.41C20.21 13.63 20.45 12.74 20.45 11.83C20.45 10.55 19.97 9.32 19.11 8.37Z M8.94734 18.1579C8.90734 18.1879 8.86734 18.2079 8.82734 18.2279C9.52734 18.8079 10.3973 19.1179 11.3073 19.1179L11.3173 19.1179C13.4573 19.1179 15.1973 17.3979 15.1973 15.2879L15.1973 10.5279C15.1973 10.5079 15.1773 10.4879 15.1573 10.4779L13.4173 9.48792L13.4173 15.2379C13.4173 15.4679 13.2873 15.6879 13.0773 15.8079L8.94734 18.1579Z M8.27654 17.0048L12.4465 14.6248C12.4665 14.6148 12.4765 14.5948 12.4765 14.5748L12.4765 14.5748L12.4765 12.5848L7.43654 15.4548C7.22654 15.5748 6.96654 15.5748 6.75654 15.4548L2.62654 13.1048C2.58654 13.0848 2.53654 13.0448 2.50654 13.0348C2.46654 13.2448 2.44654 13.4648 2.44654 13.6848C2.44654 14.3548 2.62654 15.0148 2.96654 15.6048L2.96654 15.5948C3.66654 16.7848 4.94654 17.5148 6.33654 17.5148C7.01654 17.5148 7.68654 17.3348 8.27654 17.0048Z M3.90324 5.16818C3.90324 5.12818 3.90324 5.06818 3.90324 5.02818C3.05324 5.33818 2.33324 5.92818 1.88324 6.70818L1.88324 6.70818C1.54324 7.28818 1.36324 7.94818 1.36324 8.61818C1.36324 9.98818 2.10324 11.2582 3.30324 11.9482L7.47324 14.3182C7.49324 14.3282 7.51324 14.3282 7.53324 14.3182L9.28324 13.3182L4.24324 10.4482C4.03324 10.3382 3.90324 10.1182 3.90324 9.87818L3.90324 9.87818L3.90324 5.16818Z M17.1561 8.50521L12.9761 6.1252C12.9561 6.1252 12.9361 6.1252 12.9161 6.1352L11.1761 7.1252L16.2161 9.9952C16.4261 10.1152 16.5561 10.3352 16.5561 10.5752C16.5561 10.5752 16.5561 10.5752 16.5561 10.5752L16.5561 15.4252C18.0761 14.8652 19.0961 13.4352 19.0961 11.8252C19.0961 10.4552 18.3561 9.1952 17.1561 8.50521Z M8.01418 5.82927C7.99418 5.83927 7.98418 5.85927 7.98418 5.87927L7.98418 5.87927L7.98418 7.86927L13.0242 4.99927C13.1242 4.93927 13.2442 4.90927 13.3642 4.90927C13.4842 4.90927 13.5942 4.93927 13.7042 4.99927L17.8342 7.34927C17.8742 7.36927 17.9142 7.39927 17.9542 7.41927L17.9542 7.41927C17.9842 7.20927 18.0042 6.98927 18.0042 6.76927C18.0042 4.65927 16.2642 2.93927 14.1242 2.93927C13.4442 2.93927 12.7742 3.11927 12.1842 3.44927L8.01418 5.82927Z M9.14676 1.33731C6.99676 1.33731 5.25676 3.05731 5.25676 5.16731L5.25676 9.92731C5.25676 9.94731 5.27676 9.95731 5.28676 9.96731L7.03676 10.9673L7.03676 5.22731L7.03676 5.21731C7.03676 4.98731 7.16676 4.76731 7.37676 4.64731L11.5068 2.29731C11.5468 2.26731 11.5968 2.23731 11.6268 2.22731C10.9268 1.64731 10.0468 1.33731 9.14676 1.33731Z M7.98345 11.5093L10.2235 12.7793L12.4735 11.5093L12.4735 8.9493L10.2235 7.6693L7.98345 8.9493L7.98345 11.5093Z " />
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 4.2 KiB |
@ -1,5 +1,11 @@
|
||||
import type { ChatRequest, ChatResponse } from "./api/openai/typing";
|
||||
import { Message, ModelConfig, useAccessStore, useChatStore } from "./store";
|
||||
import {
|
||||
Message,
|
||||
ModelConfig,
|
||||
ModelType,
|
||||
useAccessStore,
|
||||
useChatStore,
|
||||
} from "./store";
|
||||
import { showToast } from "./components/ui-lib";
|
||||
|
||||
const TIME_OUT_MS = 60000;
|
||||
@ -9,6 +15,7 @@ const makeRequestParam = (
|
||||
options?: {
|
||||
filterBot?: boolean;
|
||||
stream?: boolean;
|
||||
model?: ModelType;
|
||||
},
|
||||
): ChatRequest => {
|
||||
let sendMessages = messages.map((v) => ({
|
||||
@ -26,6 +33,11 @@ const makeRequestParam = (
|
||||
// @ts-expect-error
|
||||
delete modelConfig.max_tokens;
|
||||
|
||||
// override model config
|
||||
if (options?.model) {
|
||||
modelConfig.model = options.model;
|
||||
}
|
||||
|
||||
return {
|
||||
messages: sendMessages,
|
||||
stream: options?.stream,
|
||||
@ -50,7 +62,7 @@ function getHeaders() {
|
||||
|
||||
export function requestOpenaiClient(path: string) {
|
||||
return (body: any, method = "POST") =>
|
||||
fetch("/api/openai?_vercel_no_cache=1", {
|
||||
fetch("/api/openai", {
|
||||
method,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
@ -61,8 +73,16 @@ export function requestOpenaiClient(path: string) {
|
||||
});
|
||||
}
|
||||
|
||||
export async function requestChat(messages: Message[]) {
|
||||
const req: ChatRequest = makeRequestParam(messages, { filterBot: true });
|
||||
export async function requestChat(
|
||||
messages: Message[],
|
||||
options?: {
|
||||
model?: ModelType;
|
||||
},
|
||||
) {
|
||||
const req: ChatRequest = makeRequestParam(messages, {
|
||||
filterBot: true,
|
||||
model: options?.model,
|
||||
});
|
||||
|
||||
const res = await requestOpenaiClient("v1/chat/completions")(req);
|
||||
|
||||
@ -204,7 +224,13 @@ export async function requestChatStream(
|
||||
}
|
||||
}
|
||||
|
||||
export async function requestWithPrompt(messages: Message[], prompt: string) {
|
||||
export async function requestWithPrompt(
|
||||
messages: Message[],
|
||||
prompt: string,
|
||||
options?: {
|
||||
model?: ModelType;
|
||||
},
|
||||
) {
|
||||
messages = messages.concat([
|
||||
{
|
||||
role: "user",
|
||||
@ -213,7 +239,7 @@ export async function requestWithPrompt(messages: Message[], prompt: string) {
|
||||
},
|
||||
]);
|
||||
|
||||
const res = await requestChat(messages);
|
||||
const res = await requestChat(messages, options);
|
||||
|
||||
return res?.choices?.at(0)?.message?.content ?? "";
|
||||
}
|
||||
|
@ -17,6 +17,7 @@ export type Message = ChatCompletionResponseMessage & {
|
||||
streaming?: boolean;
|
||||
isError?: boolean;
|
||||
id?: number;
|
||||
model?: ModelType;
|
||||
};
|
||||
|
||||
export function createMessage(override: Partial<Message>): Message {
|
||||
@ -58,7 +59,7 @@ export interface ChatConfig {
|
||||
disablePromptHint: boolean;
|
||||
|
||||
modelConfig: {
|
||||
model: string;
|
||||
model: ModelType;
|
||||
temperature: number;
|
||||
max_tokens: number;
|
||||
presence_penalty: number;
|
||||
@ -96,7 +97,9 @@ export const ALL_MODELS = [
|
||||
name: "gpt-3.5-turbo-0301",
|
||||
available: true,
|
||||
},
|
||||
];
|
||||
] as const;
|
||||
|
||||
export type ModelType = (typeof ALL_MODELS)[number]["name"];
|
||||
|
||||
export function limitNumber(
|
||||
x: number,
|
||||
@ -119,7 +122,7 @@ export function limitModel(name: string) {
|
||||
|
||||
export const ModalConfigValidator = {
|
||||
model(x: string) {
|
||||
return limitModel(x);
|
||||
return limitModel(x) as ModelType;
|
||||
},
|
||||
max_tokens(x: number) {
|
||||
return limitNumber(x, 0, 32000, 2000);
|
||||
@ -387,6 +390,7 @@ export const useChatStore = create<ChatStore>()(
|
||||
role: "assistant",
|
||||
streaming: true,
|
||||
id: userMessage.id! + 1,
|
||||
model: get().config.modelConfig.model,
|
||||
});
|
||||
|
||||
// get recent messages
|
||||
@ -531,14 +535,14 @@ export const useChatStore = create<ChatStore>()(
|
||||
session.topic === DEFAULT_TOPIC &&
|
||||
countMessages(session.messages) >= SUMMARIZE_MIN_LEN
|
||||
) {
|
||||
requestWithPrompt(session.messages, Locale.Store.Prompt.Topic).then(
|
||||
(res) => {
|
||||
get().updateCurrentSession(
|
||||
(session) =>
|
||||
(session.topic = res ? trimTopic(res) : DEFAULT_TOPIC),
|
||||
);
|
||||
},
|
||||
);
|
||||
requestWithPrompt(session.messages, Locale.Store.Prompt.Topic, {
|
||||
model: "gpt-3.5-turbo",
|
||||
}).then((res) => {
|
||||
get().updateCurrentSession(
|
||||
(session) =>
|
||||
(session.topic = res ? trimTopic(res) : DEFAULT_TOPIC),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
const config = get().config;
|
||||
|
18
app/utils.ts
18
app/utils.ts
@ -1,4 +1,5 @@
|
||||
import { EmojiStyle } from "emoji-picker-react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { showToast } from "./components/ui-lib";
|
||||
import Locale from "./locales";
|
||||
|
||||
@ -47,6 +48,23 @@ export function isIOS() {
|
||||
return /iphone|ipad|ipod/.test(userAgent);
|
||||
}
|
||||
|
||||
export function useMobileScreen() {
|
||||
const [isMobileScreen_, setIsMobileScreen] = useState(false);
|
||||
useEffect(() => {
|
||||
const onResize = () => {
|
||||
setIsMobileScreen(isMobileScreen());
|
||||
};
|
||||
|
||||
window.addEventListener("resize", onResize);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("resize", onResize);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return isMobileScreen_;
|
||||
}
|
||||
|
||||
export function isMobileScreen() {
|
||||
return window.innerWidth <= 600;
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user