diff --git a/app/components/Btn/index.tsx b/app/components/Btn/index.tsx
index d05919d9c..59bea6cc0 100644
--- a/app/components/Btn/index.tsx
+++ b/app/components/Btn/index.tsx
@@ -50,7 +50,7 @@ export default function IconButton(props: {
autoFocus={autoFocus}
>
{text && (
-
+
{text}
)}
diff --git a/app/components/GlobalLoading/index.tsx b/app/components/GlobalLoading/index.tsx
new file mode 100644
index 000000000..7681f4ec6
--- /dev/null
+++ b/app/components/GlobalLoading/index.tsx
@@ -0,0 +1,18 @@
+import BotIcon from "@/app/icons/bot.svg";
+import LoadingIcon from "@/app/icons/three-dots.svg";
+
+export default function GloablLoading({
+ noLogo,
+}: {
+ noLogo?: boolean;
+ useSkeleton?: boolean;
+}) {
+ return (
+
+ {!noLogo && }
+
+
+ );
+}
diff --git a/app/components/MenuWrapper/index.tsx b/app/components/MenuWrapper/index.tsx
new file mode 100644
index 000000000..79ab3d088
--- /dev/null
+++ b/app/components/MenuWrapper/index.tsx
@@ -0,0 +1,85 @@
+import { useNavigate } from "react-router-dom";
+import { Path } from "@/app/constant";
+import useDragSideBar from "@/app/hooks/useDragSideBar";
+import useMobileScreen from "@/app/hooks/useMobileScreen";
+import {
+ ComponentType,
+ Context,
+ createContext,
+ useContext,
+ useState,
+} from "react";
+
+import DragIcon from "@/app/icons/drag.svg";
+
+export interface MenuWrapperInspectProps {
+ setShowPanel?: (v: boolean) => void;
+ showPanel?: boolean;
+}
+
+export default function MenuWrapper<
+ ListComponentProps extends MenuWrapperInspectProps,
+ PanelComponentProps extends MenuWrapperInspectProps,
+>(
+ ListComponent: ComponentType
,
+ PanelComponent: ComponentType,
+) {
+ return function MenuHood(props: ListComponentProps & PanelComponentProps) {
+ const [showPanel, setShowPanel] = useState(false);
+
+ const navigate = useNavigate();
+
+ const isMobileScreen = useMobileScreen();
+ // drag side bar
+ const { onDragStart } = useDragSideBar();
+
+ let containerClassName = "flex h-[100%] w-[100%]";
+ let listClassName =
+ "relative basis-sidebar h-[calc(100%-1.25rem)] pb-6 max-md:px-4 max-md:pb-4 rounded-md my-2.5 bg-gray-50";
+ let panelClassName = "flex-1 h-[100%] w-page";
+
+ if (isMobileScreen) {
+ containerClassName = "h-[100%] w-[100%] relative bg-center";
+ listClassName = `h-[100%] w-[100%] flex-1 px-4`;
+ panelClassName = `transition-all duration-300 absolute top-0 max-h-[100vh] w-[100%] ${
+ showPanel ? "left-0" : "left-[101%]"
+ } z-10`;
+ }
+
+ return (
+
+
{
+ if (e.target === e.currentTarget) {
+ navigate(Path.Home);
+ }
+ }}
+ >
+
+ {!isMobileScreen && (
+
onDragStart(e as any)}
+ >
+
+
+
+
+ )}
+
+
+
+ );
+ };
+}
diff --git a/app/components/Screen/index.tsx b/app/components/Screen/index.tsx
index 617a10a92..8650eb4c0 100644
--- a/app/components/Screen/index.tsx
+++ b/app/components/Screen/index.tsx
@@ -18,7 +18,6 @@ interface ScreenProps {
export default function Screen(props: ScreenProps) {
const location = useLocation();
const isAuth = location.pathname === Path.Auth;
- const isHome = location.pathname === Path.Home;
const isMobileScreen = useMobileScreen();
const isIOSMobile = useMemo(
@@ -28,16 +27,15 @@ export default function Screen(props: ScreenProps) {
useListenWinResize();
- let containerClassName = "flex h-[100%] w-[100%]";
- let pageClassName = "flex-1 h-[100%] w-page";
- let sidebarClassName = "basis-sidebar h-[100%]";
+ let containerClassName = "flex h-[100%] w-[100%] bg-center overflow-hidden";
+ let sidebarClassName = "flex-0 overflow-hidden";
+ let pageClassName = "flex-1 h-[100%] min-w-0 overflow-hidden";
if (isMobileScreen) {
- containerClassName = "h-[100%] w-[100%] relative bg-center";
- pageClassName = `absolute top-0 h-[100%] w-[100%] ${
- !isHome ? "left-0" : "left-[101%]"
- } z-10`;
- sidebarClassName = `h-[100%] w-[100%]`;
+ containerClassName =
+ "relative flex flex-col-reverse h-[100%] w-[100%] bg-center";
+ sidebarClassName = "absolute w-[100%] bottom-0 z-10";
+ pageClassName = "w-[100%] h-[100%]";
}
return (
diff --git a/app/components/mask.tsx b/app/components/mask.tsx
index 32a16c942..537fccd14 100644
--- a/app/components/mask.tsx
+++ b/app/components/mask.tsx
@@ -1,5 +1,4 @@
import { IconButton } from "./button";
-import { ErrorBoundary } from "./error";
import styles from "./mask.module.scss";
@@ -56,6 +55,7 @@ import {
OnDragEndResponder,
} from "@hello-pangea/dnd";
import { getMessageTextContent } from "../utils";
+import useMobileScreen from "@/app/hooks/useMobileScreen";
// drag and drop helper function
function reorder(list: T[], startIndex: number, endIndex: number): T[] {
@@ -465,9 +465,15 @@ export function MaskPage() {
});
};
+ const isMobileScreen = useMobileScreen();
+
return (
-
-
+ <>
+
@@ -645,6 +651,6 @@ export function MaskPage() {
)}
-
+ >
);
}
diff --git a/app/components/new-chat.tsx b/app/components/new-chat.tsx
index 54c646f23..aba3c5b1c 100644
--- a/app/components/new-chat.tsx
+++ b/app/components/new-chat.tsx
@@ -16,6 +16,7 @@ import { MaskAvatar } from "./mask";
import { useCommand } from "../command";
import { showConfirm } from "./ui-lib";
import { BUILTIN_MASK_STORE } from "../masks";
+import useMobileScreen from "@/app/hooks/useMobileScreen";
function MaskItem(props: { mask: Mask; onClick?: () => void }) {
return (
@@ -110,8 +111,14 @@ export function NewChat() {
}
}, [groups]);
+ const isMobileScreen = useMobileScreen();
+
return (
-
+
}
diff --git a/app/constant.ts b/app/constant.ts
index a5a39a2d0..a94a23b86 100644
--- a/app/constant.ts
+++ b/app/constant.ts
@@ -49,9 +49,9 @@ export enum StoreKey {
Sync = "sync",
}
-export const DEFAULT_SIDEBAR_WIDTH = 404;
-export const MAX_SIDEBAR_WIDTH = 504;
-export const MIN_SIDEBAR_WIDTH = 294;
+export const DEFAULT_SIDEBAR_WIDTH = 340;
+export const MAX_SIDEBAR_WIDTH = 440;
+export const MIN_SIDEBAR_WIDTH = 230;
export const WINDOW_WIDTH_SM = 480;
export const WINDOW_WIDTH_MD = 768;
diff --git a/app/containers/Chat/ChatAction.tsx b/app/containers/Chat/ChatAction.tsx
deleted file mode 100644
index 25c69954c..000000000
--- a/app/containers/Chat/ChatAction.tsx
+++ /dev/null
@@ -1,52 +0,0 @@
-import { useRef, useState } from "react";
-
-import styles from "./index.module.scss";
-
-export default function ChatAction(props: {
- text: string;
- icon: JSX.Element;
- onClick: () => void;
-}) {
- const iconRef = useRef
(null);
- const textRef = useRef(null);
- const [width, setWidth] = useState({
- full: 16,
- icon: 16,
- });
-
- function updateWidth() {
- if (!iconRef.current || !textRef.current) return;
- const getWidth = (dom: HTMLDivElement) => dom.getBoundingClientRect().width;
- const textWidth = getWidth(textRef.current);
- const iconWidth = getWidth(iconRef.current);
- setWidth({
- full: textWidth + iconWidth,
- icon: iconWidth,
- });
- }
-
- return (
- {
- props.onClick();
- setTimeout(updateWidth, 1);
- }}
- onMouseEnter={updateWidth}
- onTouchStart={updateWidth}
- style={
- {
- "--icon-width": `${width.icon}px`,
- "--full-width": `${width.full}px`,
- } as React.CSSProperties
- }
- >
-
- {props.icon}
-
-
- {props.text}
-
-
- );
-}
diff --git a/app/containers/Chat/ChatActions.tsx b/app/containers/Chat/ChatActions.tsx
index 262451884..3e53eba27 100644
--- a/app/containers/Chat/ChatActions.tsx
+++ b/app/containers/Chat/ChatActions.tsx
@@ -176,19 +176,22 @@ export function ChatActions(props: {
if (props.isMobileScreen) {
const content = (
- {actions.map((act) => {
- return (
-
- {act.icon}
-
- {act.text}
+ {actions
+ .filter((v) => v.isShow)
+ .map((act) => {
+ return (
+
+ {act.icon}
+
+ {act.text}
+
-
- );
- })}
+ );
+ })}
);
return (
diff --git a/app/containers/Chat/ChatInputPanel.tsx b/app/containers/Chat/ChatInputPanel.tsx
index 9d783165d..44593faa6 100644
--- a/app/containers/Chat/ChatInputPanel.tsx
+++ b/app/containers/Chat/ChatInputPanel.tsx
@@ -1,4 +1,4 @@
-import { forwardRef, useImperativeHandle, useMemo, useState } from "react";
+import { forwardRef, useImperativeHandle, useState } from "react";
import { useNavigate } from "react-router-dom";
import { useDebouncedCallback } from "use-debounce";
import useUploadImage from "@/app/hooks/useUploadImage";
@@ -10,7 +10,6 @@ import { ChatCommandPrefix, useChatCommand } from "@/app/command";
import { useChatStore } from "@/app/store/chat";
import { usePromptStore } from "@/app/store/prompt";
import { useAppConfig } from "@/app/store/config";
-import useScrollToBottom from "@/app/hooks/useScrollToBottom";
import usePaste from "@/app/hooks/usePaste";
import { ChatActions } from "./ChatActions";
@@ -168,10 +167,14 @@ export default forwardRef
(
useImperativeHandle(ref, () => ({
setUploading,
doSubmit,
- setAutoScroll,
setMsgRenderIndex,
}));
+ function scrollToBottom() {
+ setMsgRenderIndex(renderMessages.length - CHAT_PAGE_SIZE);
+ scrollDomToBottom();
+ }
+
const onInput = (text: string) => {
setUserInput(text);
const n = text.trim().length;
@@ -196,11 +199,6 @@ export default forwardRef(
_setMsgRenderIndex(newIndex);
}
- function scrollToBottom() {
- setMsgRenderIndex(renderMessages.length - CHAT_PAGE_SIZE);
- scrollDomToBottom();
- }
-
const { handlePaste } = usePaste(attachImages, {
emitImages: setAttachImages,
setUploading,
diff --git a/app/containers/Chat/ChatPanel.tsx b/app/containers/Chat/ChatPanel.tsx
new file mode 100644
index 000000000..50933e63e
--- /dev/null
+++ b/app/containers/Chat/ChatPanel.tsx
@@ -0,0 +1,334 @@
+import React, { useState, useRef, useEffect, useMemo } from "react";
+import {
+ useChatStore,
+ BOT_HELLO,
+ createMessage,
+ useAccessStore,
+ useAppConfig,
+ ModelType,
+} from "@/app/store";
+import Locale from "@/app/locales";
+import { Selector, showConfirm, showToast } from "@/app/components/ui-lib";
+import {
+ CHAT_PAGE_SIZE,
+ REQUEST_TIMEOUT_MS,
+ UNFINISHED_INPUT,
+} from "@/app/constant";
+import { useCommand } from "@/app/command";
+import { prettyObject } from "@/app/utils/format";
+import { ExportMessageModal } from "@/app/components/exporter";
+
+import PromptToast from "./PromptToast";
+import { EditMessageModal } from "./EditMessageModal";
+import ChatHeader from "./ChatHeader";
+import ChatInputPanel, { ChatInputPanelInstance } from "./ChatInputPanel";
+import ChatMessagePanel, { RenderMessage } from "./ChatMessagePanel";
+import { useAllModels } from "@/app/utils/hooks";
+import useRows from "@/app/hooks/useRows";
+import useMobileScreen from "@/app/hooks/useMobileScreen";
+import SessionConfigModel from "./SessionConfigModal";
+import useScrollToBottom from "@/app/hooks/useScrollToBottom";
+
+function _Chat() {
+ const chatStore = useChatStore();
+ const session = chatStore.currentSession();
+ const config = useAppConfig();
+
+ const [showExport, setShowExport] = useState(false);
+ const [showModelSelector, setShowModelSelector] = useState(false);
+
+ const inputRef = useRef(null);
+ const [userInput, setUserInput] = useState("");
+ const [isLoading, setIsLoading] = useState(false);
+ const scrollRef = useRef(null);
+ const chatInputPanelRef = useRef(null);
+
+ const [hitBottom, setHitBottom] = useState(true);
+ const isMobileScreen = useMobileScreen();
+
+ const [attachImages, setAttachImages] = useState([]);
+
+ // auto grow input
+ const { measure, inputRows } = useRows({
+ inputRef,
+ });
+
+ const { setAutoScroll, scrollDomToBottom } = useScrollToBottom(scrollRef);
+
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ useEffect(measure, [userInput]);
+
+ useEffect(() => {
+ chatStore.updateCurrentSession((session) => {
+ const stopTiming = Date.now() - REQUEST_TIMEOUT_MS;
+ session.messages.forEach((m) => {
+ // check if should stop all stale messages
+ if (m.isError || new Date(m.date).getTime() < stopTiming) {
+ if (m.streaming) {
+ m.streaming = false;
+ }
+
+ if (m.content.length === 0) {
+ m.isError = true;
+ m.content = prettyObject({
+ error: true,
+ message: "empty response",
+ });
+ }
+ }
+ });
+
+ // auto sync mask config from global config
+ if (session.mask.syncGlobalConfig) {
+ console.log("[Mask] syncing from global, name = ", session.mask.name);
+ session.mask.modelConfig = { ...config.modelConfig };
+ }
+ });
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, []);
+
+ const context: RenderMessage[] = useMemo(() => {
+ return session.mask.hideContext ? [] : session.mask.context.slice();
+ }, [session.mask.context, session.mask.hideContext]);
+ const accessStore = useAccessStore();
+
+ if (
+ context.length === 0 &&
+ session.messages.at(0)?.content !== BOT_HELLO.content
+ ) {
+ const copiedHello = Object.assign({}, BOT_HELLO);
+ if (!accessStore.isAuthorized()) {
+ copiedHello.content = Locale.Error.Unauthorized;
+ }
+ context.push(copiedHello);
+ }
+
+ // preview messages
+ const renderMessages = useMemo(() => {
+ return context
+ .concat(session.messages as RenderMessage[])
+ .concat(
+ isLoading
+ ? [
+ {
+ ...createMessage({
+ role: "assistant",
+ content: "……",
+ }),
+ preview: true,
+ },
+ ]
+ : [],
+ )
+ .concat(
+ userInput.length > 0 && config.sendPreviewBubble
+ ? [
+ {
+ ...createMessage(
+ {
+ role: "user",
+ content: userInput,
+ },
+ {
+ customId: "typing",
+ },
+ ),
+ preview: true,
+ },
+ ]
+ : [],
+ );
+ }, [
+ config.sendPreviewBubble,
+ context,
+ isLoading,
+ session.messages,
+ userInput,
+ ]);
+
+ const [msgRenderIndex, _setMsgRenderIndex] = useState(
+ Math.max(0, renderMessages.length - CHAT_PAGE_SIZE),
+ );
+
+ const [showPromptModal, setShowPromptModal] = useState(false);
+
+ useCommand({
+ fill: setUserInput,
+ submit: (text) => {
+ chatInputPanelRef.current?.doSubmit(text);
+ },
+ code: (text) => {
+ if (accessStore.disableFastLink) return;
+ console.log("[Command] got code from url: ", text);
+ showConfirm(Locale.URLCommand.Code + `code = ${text}`).then((res) => {
+ if (res) {
+ accessStore.update((access) => (access.accessCode = text));
+ }
+ });
+ },
+ settings: (text) => {
+ if (accessStore.disableFastLink) return;
+
+ try {
+ const payload = JSON.parse(text) as {
+ key?: string;
+ url?: string;
+ };
+
+ console.log("[Command] got settings from url: ", payload);
+
+ if (payload.key || payload.url) {
+ showConfirm(
+ Locale.URLCommand.Settings +
+ `\n${JSON.stringify(payload, null, 4)}`,
+ ).then((res) => {
+ if (!res) return;
+ if (payload.key) {
+ accessStore.update(
+ (access) => (access.openaiApiKey = payload.key!),
+ );
+ }
+ if (payload.url) {
+ accessStore.update((access) => (access.openaiUrl = payload.url!));
+ }
+ });
+ }
+ } catch {
+ console.error("[Command] failed to get settings from url: ", text);
+ }
+ },
+ });
+
+ // edit / insert message modal
+ const [isEditingMessage, setIsEditingMessage] = useState(false);
+
+ // remember unfinished input
+ useEffect(() => {
+ // try to load from local storage
+ const key = UNFINISHED_INPUT(session.id);
+ const mayBeUnfinishedInput = localStorage.getItem(key);
+ if (mayBeUnfinishedInput && userInput.length === 0) {
+ setUserInput(mayBeUnfinishedInput);
+ localStorage.removeItem(key);
+ }
+
+ const dom = inputRef.current;
+ return () => {
+ localStorage.setItem(key, dom?.value ?? "");
+ };
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, []);
+
+ const chatinputPanelProps = {
+ inputRef,
+ isMobileScreen,
+ renderMessages,
+ attachImages,
+ userInput,
+ hitBottom,
+ inputRows,
+ setAttachImages,
+ setUserInput,
+ setIsLoading,
+ showChatSetting: setShowPromptModal,
+ _setMsgRenderIndex,
+ showModelSelector: setShowModelSelector,
+ scrollDomToBottom,
+ setAutoScroll,
+ };
+
+ const chatMessagePanelProps = {
+ scrollRef,
+ inputRef,
+ isMobileScreen,
+ msgRenderIndex,
+ userInput,
+ context,
+ renderMessages,
+ setAutoScroll,
+ setMsgRenderIndex: chatInputPanelRef.current?.setMsgRenderIndex,
+ setHitBottom,
+ setUserInput,
+ setIsLoading,
+ setShowPromptModal,
+ scrollDomToBottom,
+ };
+
+ const currentModel = chatStore.currentSession().mask.modelConfig.model;
+ const allModels = useAllModels();
+ const models = useMemo(
+ () => allModels.filter((m) => m.available),
+ [allModels],
+ );
+
+ return (
+
+
+
+
+
+
+
+ {showExport && (
+
setShowExport(false)} />
+ )}
+
+ {isEditingMessage && (
+ {
+ setIsEditingMessage(false);
+ }}
+ />
+ )}
+
+
+
+ {showPromptModal && (
+ setShowPromptModal(false)} />
+ )}
+
+ {showModelSelector && (
+ ({
+ title: m.displayName,
+ value: m.name,
+ }))}
+ onClose={() => setShowModelSelector(false)}
+ onSelection={(s) => {
+ if (s.length === 0) return;
+ chatStore.updateCurrentSession((session) => {
+ session.mask.modelConfig.model = s[0] as ModelType;
+ session.mask.syncGlobalConfig = false;
+ });
+ showToast(s[0]);
+ }}
+ />
+ )}
+
+ );
+}
+
+export default function Chat() {
+ const chatStore = useChatStore();
+ const sessionIndex = chatStore.currentSessionIndex;
+ return <_Chat key={sessionIndex}>;
+}
diff --git a/app/containers/Chat/MessageActions.tsx b/app/containers/Chat/MessageActions.tsx
index 300874c4c..2341808b6 100644
--- a/app/containers/Chat/MessageActions.tsx
+++ b/app/containers/Chat/MessageActions.tsx
@@ -49,7 +49,7 @@ const genActionsShema = (
(message: RenderMessage) => void
>,
) => {
- const className = "!p-1 hover:bg-gray-100 !rounded-actions-bar-btn";
+ const className = " !p-1 hover:bg-gray-100 !rounded-actions-bar-btn ";
return [
{
id: "Edit",
@@ -231,9 +231,19 @@ export default function MessageActions(props: MessageActionsProps) {
return (
showActions && (
(null);
- const [userInput, setUserInput] = useState("");
- const [isLoading, setIsLoading] = useState(false);
- const scrollRef = useRef(null);
- const chatInputPanelRef = useRef(null);
-
- const [hitBottom, setHitBottom] = useState(true);
- const isMobileScreen = useMobileScreen();
-
- const [attachImages, setAttachImages] = useState([]);
-
- // auto grow input
- const { measure, inputRows } = useRows({
- inputRef,
- });
-
- const { setAutoScroll, scrollDomToBottom } = useScrollToBottom(scrollRef);
-
- // eslint-disable-next-line react-hooks/exhaustive-deps
- useEffect(measure, [userInput]);
+import MenuWrapper, {
+ MenuWrapperInspectProps,
+} from "@/app/components/MenuWrapper";
+import Panel from "./ChatPanel";
+export function SessionItem(props: {
+ onClick?: () => void;
+ onDelete?: () => void;
+ title: string;
+ count: number;
+ time: string;
+ selected: boolean;
+ id: string;
+ index: number;
+ narrow?: boolean;
+ mask: Mask;
+}) {
+ const draggableRef = useRef(null);
useEffect(() => {
- chatStore.updateCurrentSession((session) => {
- const stopTiming = Date.now() - REQUEST_TIMEOUT_MS;
- session.messages.forEach((m) => {
- // check if should stop all stale messages
- if (m.isError || new Date(m.date).getTime() < stopTiming) {
- if (m.streaming) {
- m.streaming = false;
- }
-
- if (m.content.length === 0) {
- m.isError = true;
- m.content = prettyObject({
- error: true,
- message: "empty response",
- });
- }
- }
+ if (props.selected && draggableRef.current) {
+ draggableRef.current?.scrollIntoView({
+ block: "center",
});
-
- // auto sync mask config from global config
- if (session.mask.syncGlobalConfig) {
- console.log("[Mask] syncing from global, name = ", session.mask.name);
- session.mask.modelConfig = { ...config.modelConfig };
- }
- });
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, []);
-
- const context: RenderMessage[] = useMemo(() => {
- return session.mask.hideContext ? [] : session.mask.context.slice();
- }, [session.mask.context, session.mask.hideContext]);
- const accessStore = useAccessStore();
-
- if (
- context.length === 0 &&
- session.messages.at(0)?.content !== BOT_HELLO.content
- ) {
- const copiedHello = Object.assign({}, BOT_HELLO);
- if (!accessStore.isAuthorized()) {
- copiedHello.content = Locale.Error.Unauthorized;
}
- context.push(copiedHello);
- }
+ }, [props.selected]);
- // preview messages
- const renderMessages = useMemo(() => {
- return context
- .concat(session.messages as RenderMessage[])
- .concat(
- isLoading
- ? [
- {
- ...createMessage({
- role: "assistant",
- content: "……",
- }),
- preview: true,
- },
- ]
- : [],
- )
- .concat(
- userInput.length > 0 && config.sendPreviewBubble
- ? [
- {
- ...createMessage({
- role: "user",
- content: userInput,
- }),
- preview: true,
- },
- ]
- : [],
- );
- }, [
- config.sendPreviewBubble,
- context,
- isLoading,
- session.messages,
- userInput,
- ]);
-
- const [msgRenderIndex, _setMsgRenderIndex] = useState(
- Math.max(0, renderMessages.length - CHAT_PAGE_SIZE),
- );
-
- const [showPromptModal, setShowPromptModal] = useState(false);
-
- useCommand({
- fill: setUserInput,
- submit: (text) => {
- chatInputPanelRef.current?.doSubmit(text);
- },
- code: (text) => {
- if (accessStore.disableFastLink) return;
- console.log("[Command] got code from url: ", text);
- showConfirm(Locale.URLCommand.Code + `code = ${text}`).then((res) => {
- if (res) {
- accessStore.update((access) => (access.accessCode = text));
- }
- });
- },
- settings: (text) => {
- if (accessStore.disableFastLink) return;
-
- try {
- const payload = JSON.parse(text) as {
- key?: string;
- url?: string;
- };
-
- console.log("[Command] got settings from url: ", payload);
-
- if (payload.key || payload.url) {
- showConfirm(
- Locale.URLCommand.Settings +
- `\n${JSON.stringify(payload, null, 4)}`,
- ).then((res) => {
- if (!res) return;
- if (payload.key) {
- accessStore.update(
- (access) => (access.openaiApiKey = payload.key!),
- );
- }
- if (payload.url) {
- accessStore.update((access) => (access.openaiUrl = payload.url!));
- }
- });
- }
- } catch {
- console.error("[Command] failed to get settings from url: ", text);
- }
- },
- });
-
- // edit / insert message modal
- const [isEditingMessage, setIsEditingMessage] = useState(false);
-
- // remember unfinished input
- useEffect(() => {
- // try to load from local storage
- const key = UNFINISHED_INPUT(session.id);
- const mayBeUnfinishedInput = localStorage.getItem(key);
- if (mayBeUnfinishedInput && userInput.length === 0) {
- setUserInput(mayBeUnfinishedInput);
- localStorage.removeItem(key);
- }
-
- const dom = inputRef.current;
- return () => {
- localStorage.setItem(key, dom?.value ?? "");
- };
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, []);
-
- const chatinputPanelProps = {
- inputRef,
- isMobileScreen,
- renderMessages,
- attachImages,
- userInput,
- hitBottom,
- inputRows,
- setAttachImages,
- setUserInput,
- setIsLoading,
- showChatSetting: setShowPromptModal,
- _setMsgRenderIndex,
- showModelSelector: setShowModelSelector,
- scrollDomToBottom,
- setAutoScroll,
- };
-
- const chatMessagePanelProps = {
- scrollRef,
- inputRef,
- isMobileScreen,
- msgRenderIndex,
- userInput,
- context,
- renderMessages,
- setAutoScroll,
- setMsgRenderIndex: chatInputPanelRef.current?.setMsgRenderIndex,
- setHitBottom,
- setUserInput,
- setIsLoading,
- setShowPromptModal,
- scrollDomToBottom,
- };
-
- const currentModel = chatStore.currentSession().mask.modelConfig.model;
- const allModels = useAllModels();
- const models = useMemo(
- () => allModels.filter((m) => m.available),
- [allModels],
- );
+ const { pathname: currentPath } = useLocation();
return (
-
-
-
-
-
-
-
- {showExport && (
-
setShowExport(false)} />
- )}
-
- {isEditingMessage && (
- {
- setIsEditingMessage(false);
+
+ {(provided) => (
+ {
+ draggableRef.current = ele;
+ provided.innerRef(ele);
}}
- />
- )}
+ {...provided.draggableProps}
+ {...provided.dragHandleProps}
+ title={`${props.title}\n${Locale.ChatItem.ChatItemCount(
+ props.count,
+ )}`}
+ >
+
+
+
+
+
+
+ {props.title}
+
+
+ {getTime(props.time)}
+
+
+
+ {Locale.ChatItem.ChatItemCount(props.count)}
+
+
-
-
- {showPromptModal && (
-
setShowPromptModal(false)} />
+ {
+ props.onDelete?.();
+ e.preventDefault();
+ e.stopPropagation();
+ }}
+ >
+
+
+
)}
-
- {showModelSelector && (
- ({
- title: m.displayName,
- value: m.name,
- }))}
- onClose={() => setShowModelSelector(false)}
- onSelection={(s) => {
- if (s.length === 0) return;
- chatStore.updateCurrentSession((session) => {
- session.mask.modelConfig.model = s[0] as ModelType;
- session.mask.syncGlobalConfig = false;
- });
- showToast(s[0]);
- }}
- />
- )}
-
+
);
}
-export default function Chat() {
+export default MenuWrapper(function SessionList(props) {
+ const { setShowPanel } = props;
+
+ const [sessions, selectedIndex, selectSession, moveSession] = useChatStore(
+ (state) => [
+ state.sessions,
+ state.currentSessionIndex,
+ state.selectSession,
+ state.moveSession,
+ ],
+ );
+ const navigate = useNavigate();
+ const isMobileScreen = useMobileScreen();
+ const config = useAppConfig();
const chatStore = useChatStore();
- const sessionIndex = chatStore.currentSessionIndex;
- return <_Chat key={sessionIndex}>;
-}
+ const { pathname: currentPath } = useLocation();
+
+ useEffect(() => {
+ setShowPanel?.(currentPath === Path.Chat);
+ }, [currentPath]);
+
+ const onDragEnd: OnDragEndResponder = (result) => {
+ const { destination, source } = result;
+ if (!destination) {
+ return;
+ }
+
+ if (
+ destination.droppableId === source.droppableId &&
+ destination.index === source.index
+ ) {
+ return;
+ }
+ moveSession(source.index, destination.index);
+ };
+
+ let layoutClassName = "flex flex-col py-7 px-0";
+
+ if (isMobileScreen) {
+ layoutClassName = "flex flex-col py-6 pb-chat-panel-mobile ";
+ }
+
+ return (
+
+
+
+
+
+
+
{
+ if (config.dontShowMaskSplashScreen) {
+ chatStore.newSession();
+ navigate(Path.Chat);
+ } else {
+ navigate(Path.NewChat);
+ }
+ }}
+ >
+
+
+
+
+ Build your own AI assistant.
+
+
+
+
{
+ if (e.target === e.currentTarget) {
+ navigate(Path.Home);
+ }
+ }}
+ >
+
+
+ {(provided) => (
+
+ {sessions.map((item, i) => (
+ {
+ navigate(Path.Chat);
+ selectSession(i);
+ }}
+ onDelete={async () => {
+ if (
+ !isMobileScreen ||
+ (await showConfirm(Locale.Home.DeleteChat))
+ ) {
+ chatStore.deleteSession(i);
+ }
+ }}
+ mask={item.mask}
+ />
+ ))}
+ {provided.placeholder}
+
+ )}
+
+
+
+
+ );
+}, Panel);
diff --git a/app/containers/Settings/SettingHeader.tsx b/app/containers/Settings/SettingHeader.tsx
new file mode 100644
index 000000000..069808576
--- /dev/null
+++ b/app/containers/Settings/SettingHeader.tsx
@@ -0,0 +1,51 @@
+import { useNavigate } from "react-router-dom";
+import { Path } from "@/app/constant";
+import Locale from "@/app/locales";
+import GobackIcon from "@/app/icons/goback.svg";
+
+export interface ChatHeaderProps {
+ isMobileScreen: boolean;
+ goback: () => void;
+}
+
+export default function SettingHeader(props: ChatHeaderProps) {
+ const { isMobileScreen, goback } = props;
+
+ const navigate = useNavigate();
+
+ let containerClassName = "";
+ let titleClassName = "mr-4";
+ let mainTitleClassName = "";
+ let subTitleClassName = "";
+
+ if (isMobileScreen) {
+ containerClassName = "h-menu-title-mobile";
+ titleClassName = "flex flex-col items-center justify-center gap-0.5 text";
+ mainTitleClassName = "text-sm-title h-[19px] leading-5";
+ subTitleClassName = "text-sm-mobile-tab leading-4";
+ }
+
+ return (
+
+ {isMobileScreen ? (
+
goback()}
+ >
+
+
+ ) : null}
+
+
+
+ {Locale.Settings.Title}
+
+
+
+ );
+}
diff --git a/app/containers/Settings/SettingPanel.tsx b/app/containers/Settings/SettingPanel.tsx
new file mode 100644
index 000000000..8e1c19631
--- /dev/null
+++ b/app/containers/Settings/SettingPanel.tsx
@@ -0,0 +1,1251 @@
+import { useState, useEffect, useMemo } from "react";
+
+import styles from "./index.module.scss";
+
+import ResetIcon from "@/app/icons/reload.svg";
+import AddIcon from "@/app/icons/add.svg";
+import CopyIcon from "@/app/icons/copy.svg";
+import ClearIcon from "@/app/icons/clear.svg";
+import LoadingIcon from "@/app/icons/three-dots.svg";
+import EditIcon from "@/app/icons/edit.svg";
+import EyeIcon from "@/app/icons/eye.svg";
+import DownloadIcon from "@/app/icons/download.svg";
+import UploadIcon from "@/app/icons/upload.svg";
+import ConfigIcon from "@/app/icons/config.svg";
+import ConfirmIcon from "@/app/icons/confirm.svg";
+
+import ConnectionIcon from "@/app/icons/connection.svg";
+import CloudSuccessIcon from "@/app/icons/cloud-success.svg";
+import CloudFailIcon from "@/app/icons/cloud-fail.svg";
+
+import {
+ Input,
+ List,
+ ListItem,
+ Modal,
+ PasswordInput,
+ Popover,
+ Select,
+ showConfirm,
+ showToast,
+} from "@/app/components/ui-lib";
+import { ModelConfigList } from "@/app/components/model-config";
+
+import { IconButton } from "@/app/components/button";
+import {
+ SubmitKey,
+ useChatStore,
+ Theme,
+ useUpdateStore,
+ useAccessStore,
+ useAppConfig,
+} from "@/app/store";
+
+import Locale, {
+ AllLangs,
+ ALL_LANG_OPTIONS,
+ changeLang,
+ getLang,
+} from "@/app/locales";
+import { copyToClipboard } from "@/app/utils";
+import Link from "next/link";
+import {
+ Anthropic,
+ Azure,
+ Google,
+ OPENAI_BASE_URL,
+ Path,
+ RELEASE_URL,
+ STORAGE_KEY,
+ ServiceProvider,
+ SlotID,
+ UPDATE_URL,
+} from "@/app/constant";
+import { Prompt, SearchService, usePromptStore } from "@/app/store/prompt";
+import { InputRange } from "@/app/components/input-range";
+import { useNavigate } from "react-router-dom";
+import { Avatar, AvatarPicker } from "@/app/components/emoji";
+import { getClientConfig } from "@/app/config/client";
+import { useSyncStore } from "@/app/store/sync";
+import { nanoid } from "nanoid";
+import { useMaskStore } from "@/app/store/mask";
+import { ProviderType } from "@/app/utils/cloud";
+import SettingHeader from "./SettingHeader";
+import useMobileScreen from "@/app/hooks/useMobileScreen";
+import { MenuWrapperInspectProps } from "@/app/components/MenuWrapper";
+
+function EditPromptModal(props: { id: string; onClose: () => void }) {
+ const promptStore = usePromptStore();
+ const prompt = promptStore.get(props.id);
+
+ return prompt ? (
+
+ ) : null;
+}
+
+function UserPromptModal(props: { onClose?: () => void }) {
+ const promptStore = usePromptStore();
+ const userPrompts = promptStore.getUserPrompts();
+ const builtinPrompts = SearchService.builtinPrompts;
+ const allPrompts = userPrompts.concat(builtinPrompts);
+ const [searchInput, setSearchInput] = useState("");
+ const [searchPrompts, setSearchPrompts] = useState([]);
+ const prompts = searchInput.length > 0 ? searchPrompts : allPrompts;
+
+ const [editingPromptId, setEditingPromptId] = useState();
+
+ useEffect(() => {
+ if (searchInput.length > 0) {
+ const searchResult = SearchService.search(searchInput);
+ setSearchPrompts(searchResult);
+ } else {
+ setSearchPrompts([]);
+ }
+ }, [searchInput]);
+
+ return (
+
+
props.onClose?.()}
+ actions={[
+ {
+ const promptId = promptStore.add({
+ id: nanoid(),
+ createdAt: Date.now(),
+ title: "Empty Prompt",
+ content: "Empty Prompt Content",
+ });
+ setEditingPromptId(promptId);
+ }}
+ icon={}
+ bordered
+ text={Locale.Settings.Prompt.Modal.Add}
+ />,
+ ]}
+ >
+
+
setSearchInput(e.currentTarget.value)}
+ >
+
+
+ {prompts.map((v, _) => (
+
+
+
{v.title}
+
+ {v.content}
+
+
+
+
+ {v.isUser && (
+ }
+ className={styles["user-prompt-button"]}
+ onClick={() => promptStore.remove(v.id!)}
+ />
+ )}
+ {v.isUser ? (
+ }
+ className={styles["user-prompt-button"]}
+ onClick={() => setEditingPromptId(v.id)}
+ />
+ ) : (
+ }
+ className={styles["user-prompt-button"]}
+ onClick={() => setEditingPromptId(v.id)}
+ />
+ )}
+ }
+ className={styles["user-prompt-button"]}
+ onClick={() => copyToClipboard(v.content)}
+ />
+
+
+ ))}
+
+
+
+
+ {editingPromptId !== undefined && (
+
setEditingPromptId(undefined)}
+ />
+ )}
+
+ );
+}
+
+function DangerItems() {
+ const chatStore = useChatStore();
+ const appConfig = useAppConfig();
+
+ return (
+
+
+ {
+ if (await showConfirm(Locale.Settings.Danger.Reset.Confirm)) {
+ appConfig.reset();
+ }
+ }}
+ type="danger"
+ />
+
+
+ {
+ if (await showConfirm(Locale.Settings.Danger.Clear.Confirm)) {
+ chatStore.clearAllData();
+ }
+ }}
+ type="danger"
+ />
+
+
+ );
+}
+
+function CheckButton() {
+ const syncStore = useSyncStore();
+
+ const couldCheck = useMemo(() => {
+ return syncStore.cloudSync();
+ }, [syncStore]);
+
+ const [checkState, setCheckState] = useState<
+ "none" | "checking" | "success" | "failed"
+ >("none");
+
+ async function check() {
+ setCheckState("checking");
+ const valid = await syncStore.check();
+ setCheckState(valid ? "success" : "failed");
+ }
+
+ if (!couldCheck) return null;
+
+ return (
+
+ ) : checkState === "checking" ? (
+
+ ) : checkState === "success" ? (
+
+ ) : checkState === "failed" ? (
+
+ ) : (
+
+ )
+ }
+ >
+ );
+}
+
+function SyncConfigModal(props: { onClose?: () => void }) {
+ const syncStore = useSyncStore();
+
+ return (
+
+
props.onClose?.()}
+ actions={[
+ ,
+ }
+ bordered
+ text={Locale.UI.Confirm}
+ />,
+ ]}
+ >
+
+
+
+
+
+
+ {
+ syncStore.update(
+ (config) => (config.useProxy = e.currentTarget.checked),
+ );
+ }}
+ >
+
+ {syncStore.useProxy ? (
+
+ {
+ syncStore.update(
+ (config) => (config.proxyUrl = e.currentTarget.value),
+ );
+ }}
+ >
+
+ ) : null}
+
+
+ {syncStore.provider === ProviderType.WebDAV && (
+ <>
+
+
+ {
+ syncStore.update(
+ (config) =>
+ (config.webdav.endpoint = e.currentTarget.value),
+ );
+ }}
+ >
+
+
+
+ {
+ syncStore.update(
+ (config) =>
+ (config.webdav.username = e.currentTarget.value),
+ );
+ }}
+ >
+
+
+ {
+ syncStore.update(
+ (config) =>
+ (config.webdav.password = e.currentTarget.value),
+ );
+ }}
+ >
+
+
+ >
+ )}
+
+ {syncStore.provider === ProviderType.UpStash && (
+
+
+ {
+ syncStore.update(
+ (config) =>
+ (config.upstash.endpoint = e.currentTarget.value),
+ );
+ }}
+ >
+
+
+
+ {
+ syncStore.update(
+ (config) =>
+ (config.upstash.username = e.currentTarget.value),
+ );
+ }}
+ >
+
+
+ {
+ syncStore.update(
+ (config) => (config.upstash.apiKey = e.currentTarget.value),
+ );
+ }}
+ >
+
+
+ )}
+
+
+ );
+}
+
+function SyncItems() {
+ const syncStore = useSyncStore();
+ const chatStore = useChatStore();
+ const promptStore = usePromptStore();
+ const maskStore = useMaskStore();
+ const couldSync = useMemo(() => {
+ return syncStore.cloudSync();
+ }, [syncStore]);
+
+ const [showSyncConfigModal, setShowSyncConfigModal] = useState(false);
+
+ const stateOverview = useMemo(() => {
+ const sessions = chatStore.sessions;
+ const messageCount = sessions.reduce((p, c) => p + c.messages.length, 0);
+
+ return {
+ chat: sessions.length,
+ message: messageCount,
+ prompt: Object.keys(promptStore.prompts).length,
+ mask: Object.keys(maskStore.masks).length,
+ };
+ }, [chatStore.sessions, maskStore.masks, promptStore.prompts]);
+
+ return (
+ <>
+
+
+
+ }
+ text={Locale.UI.Config}
+ onClick={() => {
+ setShowSyncConfigModal(true);
+ }}
+ />
+ {couldSync && (
+ }
+ text={Locale.UI.Sync}
+ onClick={async () => {
+ try {
+ await syncStore.sync();
+ showToast(Locale.Settings.Sync.Success);
+ } catch (e) {
+ showToast(Locale.Settings.Sync.Fail);
+ console.error("[Sync]", e);
+ }
+ }}
+ />
+ )}
+
+
+
+
+
+ }
+ text={Locale.UI.Export}
+ onClick={() => {
+ syncStore.export();
+ }}
+ />
+ }
+ text={Locale.UI.Import}
+ onClick={() => {
+ syncStore.import();
+ }}
+ />
+
+
+
+
+ {showSyncConfigModal && (
+ setShowSyncConfigModal(false)} />
+ )}
+ >
+ );
+}
+
+export default function Settings(props: MenuWrapperInspectProps) {
+ const { setShowPanel } = props;
+
+ const navigate = useNavigate();
+ const [showEmojiPicker, setShowEmojiPicker] = useState(false);
+ const config = useAppConfig();
+ const updateConfig = config.update;
+
+ const isMobileScreen = useMobileScreen();
+
+ const updateStore = useUpdateStore();
+ const [checkingUpdate, setCheckingUpdate] = useState(false);
+ const currentVersion = updateStore.formatVersion(updateStore.version);
+ const remoteId = updateStore.formatVersion(updateStore.remoteVersion);
+ const hasNewVersion = currentVersion !== remoteId;
+ const updateUrl = getClientConfig()?.isApp ? RELEASE_URL : UPDATE_URL;
+
+ function checkUpdate(force = false) {
+ setCheckingUpdate(true);
+ updateStore.getLatestVersion(force).then(() => {
+ setCheckingUpdate(false);
+ });
+
+ console.log("[Update] local version ", updateStore.version);
+ console.log("[Update] remote version ", updateStore.remoteVersion);
+ }
+
+ const accessStore = useAccessStore();
+ const shouldHideBalanceQuery = useMemo(() => {
+ const isOpenAiUrl = accessStore.openaiUrl.includes(OPENAI_BASE_URL);
+
+ return (
+ accessStore.hideBalanceQuery ||
+ isOpenAiUrl ||
+ accessStore.provider === ServiceProvider.Azure
+ );
+ }, [
+ accessStore.hideBalanceQuery,
+ accessStore.openaiUrl,
+ accessStore.provider,
+ ]);
+
+ const usage = {
+ used: updateStore.used,
+ subscription: updateStore.subscription,
+ };
+ const [loadingUsage, setLoadingUsage] = useState(false);
+ function checkUsage(force = false) {
+ if (shouldHideBalanceQuery) {
+ return;
+ }
+
+ setLoadingUsage(true);
+ updateStore.updateUsage(force).finally(() => {
+ setLoadingUsage(false);
+ });
+ }
+
+ const enabledAccessControl = useMemo(
+ () => accessStore.enabledAccessControl(),
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ [],
+ );
+
+ const promptStore = usePromptStore();
+ const builtinCount = SearchService.count.builtin;
+ const customCount = promptStore.getUserPrompts().length ?? 0;
+ const [shouldShowPromptModal, setShowPromptModal] = useState(false);
+
+ const showUsage = accessStore.isAuthorized();
+ useEffect(() => {
+ // checks per minutes
+ checkUpdate();
+ showUsage && checkUsage();
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, []);
+
+ useEffect(() => {
+ const keydownEvent = (e: KeyboardEvent) => {
+ if (e.key === "Escape") {
+ navigate(Path.Home);
+ }
+ };
+ if (clientConfig?.isApp) {
+ // Force to set custom endpoint to true if it's app
+ accessStore.update((state) => {
+ state.useCustomConfig = true;
+ });
+ }
+ document.addEventListener("keydown", keydownEvent);
+ return () => {
+ document.removeEventListener("keydown", keydownEvent);
+ };
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, []);
+
+ const clientConfig = useMemo(() => getClientConfig(), []);
+ const showAccessCode = enabledAccessControl && !clientConfig?.isApp;
+
+ return (
+
+
setShowPanel?.(false)}
+ />
+
+
+
+ setShowEmojiPicker(false)}
+ content={
+ {
+ updateConfig((config) => (config.avatar = avatar));
+ setShowEmojiPicker(false);
+ }}
+ />
+ }
+ open={showEmojiPicker}
+ >
+ {
+ setShowEmojiPicker(!showEmojiPicker);
+ }}
+ >
+
+
+
+
+
+
+ {checkingUpdate ? (
+
+ ) : hasNewVersion ? (
+
+ {Locale.Settings.Update.GoToUpdate}
+
+ ) : (
+ }
+ text={Locale.Settings.Update.CheckUpdate}
+ onClick={() => checkUpdate(true)}
+ />
+ )}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ updateConfig(
+ (config) =>
+ (config.fontSize = Number.parseInt(e.currentTarget.value)),
+ )
+ }
+ >
+
+
+
+
+ updateConfig(
+ (config) =>
+ (config.enableAutoGenerateTitle = e.currentTarget.checked),
+ )
+ }
+ >
+
+
+
+
+ updateConfig(
+ (config) =>
+ (config.sendPreviewBubble = e.currentTarget.checked),
+ )
+ }
+ >
+
+
+
+
+
+
+
+
+ updateConfig(
+ (config) =>
+ (config.dontShowMaskSplashScreen =
+ !e.currentTarget.checked),
+ )
+ }
+ >
+
+
+
+
+ updateConfig(
+ (config) =>
+ (config.hideBuiltinMasks = e.currentTarget.checked),
+ )
+ }
+ >
+
+
+
+
+
+
+ updateConfig(
+ (config) =>
+ (config.disablePromptHint = e.currentTarget.checked),
+ )
+ }
+ >
+
+
+
+ }
+ text={Locale.Settings.Prompt.Edit}
+ onClick={() => setShowPromptModal(true)}
+ />
+
+
+
+
+ {showAccessCode && (
+
+ {
+ accessStore.update(
+ (access) => (access.accessCode = e.currentTarget.value),
+ );
+ }}
+ />
+
+ )}
+
+ {!accessStore.hideUserApiKey && (
+ <>
+ {
+ // Conditionally render the following ListItem based on clientConfig.isApp
+ !clientConfig?.isApp && ( // only show if isApp is false
+
+
+ accessStore.update(
+ (access) =>
+ (access.useCustomConfig = e.currentTarget.checked),
+ )
+ }
+ >
+
+ )
+ }
+ {accessStore.useCustomConfig && (
+ <>
+
+
+
+
+ {accessStore.provider === ServiceProvider.OpenAI && (
+ <>
+
+
+ accessStore.update(
+ (access) =>
+ (access.openaiUrl = e.currentTarget.value),
+ )
+ }
+ >
+
+
+ {
+ accessStore.update(
+ (access) =>
+ (access.openaiApiKey = e.currentTarget.value),
+ );
+ }}
+ />
+
+ >
+ )}
+ {accessStore.provider === ServiceProvider.Azure && (
+ <>
+
+
+ accessStore.update(
+ (access) =>
+ (access.azureUrl = e.currentTarget.value),
+ )
+ }
+ >
+
+
+ {
+ accessStore.update(
+ (access) =>
+ (access.azureApiKey = e.currentTarget.value),
+ );
+ }}
+ />
+
+
+
+ accessStore.update(
+ (access) =>
+ (access.azureApiVersion =
+ e.currentTarget.value),
+ )
+ }
+ >
+
+ >
+ )}
+ {accessStore.provider === ServiceProvider.Google && (
+ <>
+
+
+ accessStore.update(
+ (access) =>
+ (access.googleUrl = e.currentTarget.value),
+ )
+ }
+ >
+
+
+ {
+ accessStore.update(
+ (access) =>
+ (access.googleApiKey = e.currentTarget.value),
+ );
+ }}
+ />
+
+
+
+ accessStore.update(
+ (access) =>
+ (access.googleApiVersion =
+ e.currentTarget.value),
+ )
+ }
+ >
+
+ >
+ )}
+ {accessStore.provider === ServiceProvider.Anthropic && (
+ <>
+
+
+ accessStore.update(
+ (access) =>
+ (access.anthropicUrl = e.currentTarget.value),
+ )
+ }
+ >
+
+
+ {
+ accessStore.update(
+ (access) =>
+ (access.anthropicApiKey =
+ e.currentTarget.value),
+ );
+ }}
+ />
+
+
+
+ accessStore.update(
+ (access) =>
+ (access.anthropicApiVersion =
+ e.currentTarget.value),
+ )
+ }
+ >
+
+ >
+ )}
+ >
+ )}
+ >
+ )}
+
+ {!shouldHideBalanceQuery && !clientConfig?.isApp ? (
+
+ {!showUsage || loadingUsage ? (
+
+ ) : (
+ }
+ text={Locale.Settings.Usage.Check}
+ onClick={() => checkUsage(true)}
+ />
+ )}
+
+ ) : null}
+
+
+
+ config.update(
+ (config) => (config.customModels = e.currentTarget.value),
+ )
+ }
+ >
+
+
+
+
+ {
+ const modelConfig = { ...config.modelConfig };
+ updater(modelConfig);
+ config.update((config) => (config.modelConfig = modelConfig));
+ }}
+ />
+
+
+ {shouldShowPromptModal && (
+
setShowPromptModal(false)} />
+ )}
+
+
+
+
+ );
+}
diff --git a/app/containers/Settings/index.module.scss b/app/containers/Settings/index.module.scss
new file mode 100644
index 000000000..c6aec4203
--- /dev/null
+++ b/app/containers/Settings/index.module.scss
@@ -0,0 +1,74 @@
+.settings {
+ padding: 20px;
+ overflow: auto;
+}
+
+.avatar {
+ cursor: pointer;
+ position: relative;
+ z-index: 1;
+}
+
+.edit-prompt-modal {
+ display: flex;
+ flex-direction: column;
+
+ .edit-prompt-title {
+ max-width: unset;
+ margin-bottom: 20px;
+ text-align: left;
+ }
+ .edit-prompt-content {
+ max-width: unset;
+ }
+}
+
+.user-prompt-modal {
+ min-height: 40vh;
+
+ .user-prompt-search {
+ width: 100%;
+ max-width: 100%;
+ margin-bottom: 10px;
+ background-color: var(--gray);
+ }
+
+ .user-prompt-list {
+ border: var(--border-in-light);
+ border-radius: 10px;
+
+ .user-prompt-item {
+ display: flex;
+ justify-content: space-between;
+ padding: 10px;
+
+ &:not(:last-child) {
+ border-bottom: var(--border-in-light);
+ }
+
+ .user-prompt-header {
+ max-width: calc(100% - 100px);
+
+ .user-prompt-title {
+ font-size: 14px;
+ line-height: 2;
+ font-weight: bold;
+ }
+ .user-prompt-content {
+ font-size: 12px;
+ }
+ }
+
+ .user-prompt-buttons {
+ display: flex;
+ align-items: center;
+ column-gap: 2px;
+
+ .user-prompt-button {
+ //height: 100%;
+ padding: 7px;
+ }
+ }
+ }
+ }
+}
diff --git a/app/containers/Settings/index.tsx b/app/containers/Settings/index.tsx
new file mode 100644
index 000000000..e80789a47
--- /dev/null
+++ b/app/containers/Settings/index.tsx
@@ -0,0 +1,61 @@
+import Locale from "@/app/locales";
+import useMobileScreen from "@/app/hooks/useMobileScreen";
+import MenuWrapper from "@/app/components/MenuWrapper";
+
+import Panel from "./SettingPanel";
+
+import GotoIcon from "@/app/icons/goto.svg";
+
+export default MenuWrapper(function SettingList(props) {
+ const { setShowPanel } = props;
+ const isMobileScreen = useMobileScreen();
+
+ let layoutClassName = "pt-7 px-4";
+ let titleClassName = "pb-5";
+ let itemClassName = "";
+
+ if (isMobileScreen) {
+ layoutClassName = "h-[100%] mx-[-1.5rem] px-6 py-6 bg-blue-50";
+ titleClassName = "h-menu-title-mobile";
+ itemClassName = "p-4 bg-white";
+ }
+
+ return (
+
+
+
+
+ {Locale.Settings.Title}
+
+
+ {/*
+ {Locale.Settings.SubTitle}
+
*/}
+
+
+
+
{
+ setShowPanel?.(true);
+ }}
+ >
+ {Locale.Settings.GeneralSettings}
+ {isMobileScreen && }
+
+
+
+ );
+}, Panel);
diff --git a/app/containers/Sidebar/MenuWrapper.tsx b/app/containers/Sidebar/MenuWrapper.tsx
deleted file mode 100644
index 7fcc51e22..000000000
--- a/app/containers/Sidebar/MenuWrapper.tsx
+++ /dev/null
@@ -1,35 +0,0 @@
-import { Path } from "@/app/constant";
-import { ComponentType } from "react";
-import { useNavigate } from "react-router-dom";
-
-export interface MenuWrapperProps {
- show: boolean;
- wrapperClassName?: string;
-}
-
-export default function MenuWrapper(
- Component: ComponentType,
-) {
- return function MenuHood(props: MenuWrapperProps & ComponentProps) {
- const { show, wrapperClassName } = props;
-
- const navigate = useNavigate();
-
- if (!show) {
- return null;
- }
-
- return (
- {
- if (e.target === e.currentTarget) {
- navigate(Path.Home);
- }
- }}
- >
-
-
- );
- };
-}
diff --git a/app/containers/Sidebar/SessionList.tsx b/app/containers/Sidebar/SessionList.tsx
deleted file mode 100644
index 7a2480dda..000000000
--- a/app/containers/Sidebar/SessionList.tsx
+++ /dev/null
@@ -1,220 +0,0 @@
-import {
- DragDropContext,
- Droppable,
- Draggable,
- OnDragEndResponder,
-} from "@hello-pangea/dnd";
-
-import { useAppConfig, useChatStore } from "@/app/store";
-
-import Locale from "@/app/locales";
-import { useLocation, useNavigate } from "react-router-dom";
-import { Path } from "@/app/constant";
-import { Mask } from "@/app/store/mask";
-import { useRef, useEffect, useMemo } from "react";
-import { showConfirm } from "@/app/components/ui-lib";
-
-import AddIcon from "@/app/icons/addIcon.svg";
-import NextChatTitle from "@/app/icons/nextchatTitle.svg";
-import { ListHoodProps } from "./types";
-import useMobileScreen from "@/app/hooks/useMobileScreen";
-import { getTime } from "@/app/utils";
-import DeleteIcon from "@/app/icons/deleteIcon.svg";
-import LogIcon from "@/app/icons/logIcon.svg";
-
-export function SessionItem(props: {
- onClick?: () => void;
- onDelete?: () => void;
- title: string;
- count: number;
- time: string;
- selected: boolean;
- id: string;
- index: number;
- narrow?: boolean;
- mask: Mask;
-}) {
- const draggableRef = useRef(null);
- useEffect(() => {
- if (props.selected && draggableRef.current) {
- draggableRef.current?.scrollIntoView({
- block: "center",
- });
- }
- }, [props.selected]);
-
- const { pathname: currentPath } = useLocation();
-
- return (
-
- {(provided) => (
- {
- draggableRef.current = ele;
- provided.innerRef(ele);
- }}
- {...provided.draggableProps}
- {...provided.dragHandleProps}
- title={`${props.title}\n${Locale.ChatItem.ChatItemCount(
- props.count,
- )}`}
- >
-
-
-
-
-
-
- {props.title}
-
-
- {getTime(props.time)}
-
-
-
- {Locale.ChatItem.ChatItemCount(props.count)}
-
-
-
-
{
- props.onDelete?.();
- e.preventDefault();
- e.stopPropagation();
- }}
- >
-
-
-
- )}
-
- );
-}
-
-export default function SessionList(props: ListHoodProps) {
- const [sessions, selectedIndex, selectSession, moveSession] = useChatStore(
- (state) => [
- state.sessions,
- state.currentSessionIndex,
- state.selectSession,
- state.moveSession,
- ],
- );
- const chatStore = useChatStore();
- const navigate = useNavigate();
- const isMobileScreen = useMobileScreen();
-
- const config = useAppConfig();
-
- const onDragEnd: OnDragEndResponder = (result) => {
- const { destination, source } = result;
- if (!destination) {
- return;
- }
-
- if (
- destination.droppableId === source.droppableId &&
- destination.index === source.index
- ) {
- return;
- }
-
- moveSession(source.index, destination.index);
- };
-
- let layoutClassName = "py-7 px-0";
-
- if (isMobileScreen) {
- layoutClassName = "h-menu-title-mobile py-6";
- }
-
- return (
- <>
-
-
-
-
-
-
{
- if (config.dontShowMaskSplashScreen) {
- chatStore.newSession();
- navigate(Path.Chat);
- } else {
- navigate(Path.NewChat);
- }
- }}
- >
-
-
-
-
- Build your own AI assistant.
-
-
-
- {
- if (e.target === e.currentTarget) {
- navigate(Path.Home);
- }
- }}
- >
-
-
- {(provided) => (
-
- {sessions.map((item, i) => (
- {
- navigate(Path.Chat);
- selectSession(i);
- }}
- onDelete={async () => {
- if (
- !isMobileScreen ||
- (await showConfirm(Locale.Home.DeleteChat))
- ) {
- chatStore.deleteSession(i);
- }
- }}
- mask={item.mask}
- />
- ))}
- {provided.placeholder}
-
- )}
-
-
-
- >
- );
-}
diff --git a/app/containers/Sidebar/SettingList.tsx b/app/containers/Sidebar/SettingList.tsx
deleted file mode 100644
index d579ebebf..000000000
--- a/app/containers/Sidebar/SettingList.tsx
+++ /dev/null
@@ -1,5 +0,0 @@
-import { ListHoodProps } from "./types";
-
-export default function SettingList(props: ListHoodProps) {
- return <>>;
-}
diff --git a/app/containers/Sidebar/index.tsx b/app/containers/Sidebar/index.tsx
index 7613b40cc..d2c64deb0 100644
--- a/app/containers/Sidebar/index.tsx
+++ b/app/containers/Sidebar/index.tsx
@@ -1,42 +1,25 @@
-import DragIcon from "@/app/icons/drag.svg";
-import DiscoverIcon from "@/app/icons/discoverActive.svg";
-import AssistantActiveIcon from "@/app/icons/assistantActive.svg";
import GitHubIcon from "@/app/icons/githubIcon.svg";
-import SettingIcon from "@/app/icons/settingActive.svg";
+import DiscoverIcon from "@/app/icons/discoverActive.svg";
import DiscoverInactiveIcon from "@/app/icons/discoverInactive.svg";
-import AssistantInactiveIcon from "@/app/icons/assistantInactive.svg";
+import DiscoverMobileActive from "@/app/icons/discoverMobileActive.svg";
+import DiscoverMobileInactive from "@/app/icons/discoverMobileInactive.svg";
+import SettingIcon from "@/app/icons/settingActive.svg";
import SettingInactiveIcon from "@/app/icons/settingInactive.svg";
import SettingMobileActive from "@/app/icons/settingMobileActive.svg";
-import DiscoverMobileActive from "@/app/icons/discoverMobileActive.svg";
+import SettingMobileInactive from "@/app/icons/settingMobileInactive.svg";
+import AssistantActiveIcon from "@/app/icons/assistantActive.svg";
+import AssistantInactiveIcon from "@/app/icons/assistantInactive.svg";
import AssistantMobileActive from "@/app/icons/assistantMobileActive.svg";
import AssistantMobileInactive from "@/app/icons/assistantMobileInactive.svg";
import { useAppConfig } from "@/app/store";
import { Path, REPO_URL } from "@/app/constant";
import { useNavigate, useLocation } from "react-router-dom";
-import dynamic from "next/dynamic";
import useHotKey from "@/app/hooks/useHotKey";
-import useDragSideBar from "@/app/hooks/useDragSideBar";
import useMobileScreen from "@/app/hooks/useMobileScreen";
-import MenuWrapper from "./MenuWrapper";
import ActionsBar from "@/app/components/ActionsBar";
-const SessionList = MenuWrapper(
- dynamic(async () => await import("./SessionList"), {
- loading: () => null,
- }),
-);
-
-const SettingList = MenuWrapper(
- dynamic(async () => await import("./SettingList"), {
- loading: () => null,
- }),
-);
-
export function SideBar(props: { className?: string }) {
- // drag side bar
- const { onDragStart } = useDragSideBar();
-
const navigate = useNavigate();
const loc = useLocation();
@@ -56,18 +39,15 @@ export function SideBar(props: { className?: string }) {
selectedTab = Path.Settings;
break;
default:
- selectedTab = Path.Chat;
+ selectedTab = Path.Home;
}
- let containerClassName = "relative flex h-[100%] w-[100%]";
+ let containerClassName = "relative flex h-[100%]";
let tabActionsClassName = "2xl:px-5 xl:px-4 px-2 py-6";
- let menuClassName =
- "max-md:px-4 max-md:pb-4 rounded-md my-2.5 bg-gray-50 flex-1";
if (isMobileScreen) {
containerClassName = "flex flex-col-reverse w-[100%] h-[100%]";
tabActionsClassName = "bg-gray-100 rounded-tl-md rounded-tr-md h-mobile";
- menuClassName = `flex-1 px-4`;
}
return (
@@ -81,12 +61,12 @@ export function SideBar(props: { className?: string }) {
active: ,
inactive: ,
mobileActive: ,
- mobileInactive: ,
+ mobileInactive: ,
},
title: "Discover",
},
{
- id: Path.Chat,
+ id: Path.Home,
icons: {
active: ,
inactive: ,
@@ -106,7 +86,7 @@ export function SideBar(props: { className?: string }) {
active: ,
inactive: ,
mobileActive: ,
- mobileInactive: ,
+ mobileInactive: ,
},
className: "p-2",
title: "Settrings",
@@ -127,36 +107,16 @@ export function SideBar(props: { className?: string }) {
}}
groups={{
normal: [
- [Path.Chat, Path.Masks],
+ [Path.Home, Path.Masks],
["github", Path.Settings],
],
- mobile: [[Path.Chat, Path.Masks, Path.Settings]],
+ mobile: [[Path.Home, Path.Masks, Path.Settings]],
}}
selected={selectedTab}
className={`${
isMobileScreen ? "justify-around" : "flex-col"
} ${tabActionsClassName}`}
/>
-
-
-
-
- {!isMobileScreen && (
- onDragStart(e as any)}
- >
-
-
-
-
- )}
);
}
diff --git a/app/containers/Sidebar/types.ts b/app/containers/Sidebar/types.ts
deleted file mode 100644
index 73325ab60..000000000
--- a/app/containers/Sidebar/types.ts
+++ /dev/null
@@ -1,4 +0,0 @@
-export interface ListHoodProps {
- // narrow?: boolean;
- className?: string;
-}
diff --git a/app/containers/index.tsx b/app/containers/index.tsx
index d9d7a67ca..50015d5d4 100644
--- a/app/containers/index.tsx
+++ b/app/containers/index.tsx
@@ -17,9 +17,10 @@ import { useLoadData } from "@/app/hooks/useLoadData";
import Loading from "@/app/components/Loading";
import Screen from "@/app/components/Screen";
import { SideBar } from "./Sidebar";
+import GlobalLoading from "@/app/components/GlobalLoading";
const Settings = dynamic(
- async () => (await import("@/app/components/settings")).Settings,
+ async () => await import("@/app/containers/Settings"),
{
loading: () => ,
},
@@ -94,7 +95,7 @@ export default function Home() {
}, []);
if (!useHasHydrated()) {
- return ;
+ return ;
}
return (
diff --git a/app/hooks/useListenWinResize.ts b/app/hooks/useListenWinResize.ts
index 156775ed5..d0ea816d0 100644
--- a/app/hooks/useListenWinResize.ts
+++ b/app/hooks/useListenWinResize.ts
@@ -10,7 +10,6 @@ import {
MIN_SIDEBAR_WIDTH,
} from "@/app/constant";
import { useAppConfig } from "../store/config";
-import { useReducer, useState } from "react";
export const MOBILE_MAX_WIDTH = 768;
@@ -25,8 +24,6 @@ const widths = [
export default function useListenWinResize() {
const config = useAppConfig();
- const [_, refresh] = useReducer((x) => x + 1, 0);
-
useWindowSize((size) => {
let nextSidebar = config.sidebarWidth;
if (!nextSidebar) {
@@ -56,6 +53,5 @@ export default function useListenWinResize() {
config.update((config) => {
config.sidebarWidth = nextSidebar;
});
- refresh();
});
}
diff --git a/app/hooks/useMobileScreen.ts b/app/hooks/useMobileScreen.ts
index ec365b762..7d1aee590 100644
--- a/app/hooks/useMobileScreen.ts
+++ b/app/hooks/useMobileScreen.ts
@@ -1,16 +1,9 @@
import { useWindowSize } from "@/app/hooks/useWindowSize";
-import { useRef } from "react";
export const MOBILE_MAX_WIDTH = 768;
export default function useMobileScreen() {
- const widthRef = useRef(0);
+ const { width } = useWindowSize();
- useWindowSize((size) => {
- widthRef.current = size.width;
- });
-
- const isMobile = widthRef.current <= MOBILE_MAX_WIDTH;
-
- return isMobile;
+ return width <= MOBILE_MAX_WIDTH;
}
diff --git a/app/hooks/useScrollToBottom.ts b/app/hooks/useScrollToBottom.ts
index 7fd604cda..f2c35348a 100644
--- a/app/hooks/useScrollToBottom.ts
+++ b/app/hooks/useScrollToBottom.ts
@@ -10,9 +10,13 @@ export default function useScrollToBottom(
) <= 1
: false;
- const initScrolled = useRef(false);
// for auto-scroll
const [autoScroll, setAutoScroll] = useState(true);
+
+ const autoScrollRef = useRef();
+
+ autoScrollRef.current = autoScroll;
+
function scrollDomToBottom() {
const dom = scrollRef.current;
if (dom) {
@@ -23,13 +27,30 @@ export default function useScrollToBottom(
}
}
+ // useEffect(() => {
+ // const dom = scrollRef.current;
+ // if (dom) {
+ // dom.ontouchstart = (e) => {
+ // const autoScroll = autoScrollRef.current;
+ // if (autoScroll) {
+ // setAutoScroll(false);
+ // }
+ // }
+ // dom.onscroll = (e) => {
+ // const autoScroll = autoScrollRef.current;
+ // if (autoScroll) {
+ // setAutoScroll(false);
+ // }
+ // }
+ // }
+ // }, []);
+
// auto scroll
useEffect(() => {
- if (autoScroll && !detach && !initScrolled.current) {
+ if (autoScroll && !detach) {
scrollDomToBottom();
- initScrolled.current = true;
}
- }, [autoScroll, detach]);
+ });
return {
scrollRef,
diff --git a/app/hooks/useWindowSize.ts b/app/hooks/useWindowSize.ts
index 20ad18a5a..a4dbf2ef9 100644
--- a/app/hooks/useWindowSize.ts
+++ b/app/hooks/useWindowSize.ts
@@ -1,26 +1,35 @@
-import { useLayoutEffect, useRef } from "react";
+import { useLayoutEffect, useRef, useState } from "react";
type Size = {
width: number;
height: number;
};
-export function useWindowSize(callback: (size: Size) => void) {
+export function useWindowSize(callback?: (size: Size) => void) {
const callbackRef = useRef();
callbackRef.current = callback;
+ const [size, setSize] = useState({
+ width: window.innerWidth,
+ height: window.innerHeight,
+ });
+
useLayoutEffect(() => {
const onResize = () => {
callbackRef.current?.({
width: window.innerWidth,
height: window.innerHeight,
});
+ setSize({
+ width: window.innerWidth,
+ height: window.innerHeight,
+ });
};
window.addEventListener("resize", onResize);
- callback({
+ callback?.({
width: window.innerWidth,
height: window.innerHeight,
});
@@ -29,4 +38,6 @@ export function useWindowSize(callback: (size: Size) => void) {
window.removeEventListener("resize", onResize);
};
}, []);
+
+ return size;
}
diff --git a/app/icons/assistantMobileInactive.svg b/app/icons/assistantMobileInactive.svg
index d76154b51..cbe807278 100644
--- a/app/icons/assistantMobileInactive.svg
+++ b/app/icons/assistantMobileInactive.svg
@@ -1,3 +1,7 @@
-
\ No newline at end of file
+
diff --git a/app/icons/discoverMobileActive.svg b/app/icons/discoverMobileActive.svg
index c341db4ec..7809e40a2 100644
--- a/app/icons/discoverMobileActive.svg
+++ b/app/icons/discoverMobileActive.svg
@@ -1,9 +1,9 @@
-
diff --git a/app/icons/discoverMobileInactive.svg b/app/icons/discoverMobileInactive.svg
index 2781d9fdf..099d32bab 100644
--- a/app/icons/discoverMobileInactive.svg
+++ b/app/icons/discoverMobileInactive.svg
@@ -1,7 +1,8 @@
-
-
-
+
+
+
-
-
+
+
+
diff --git a/app/icons/goto.svg b/app/icons/goto.svg
new file mode 100644
index 000000000..29ff0ebb5
--- /dev/null
+++ b/app/icons/goto.svg
@@ -0,0 +1,3 @@
+
+
+
diff --git a/app/icons/settingMobileActive.svg b/app/icons/settingMobileActive.svg
index 61b1e44c5..eed56e59d 100644
--- a/app/icons/settingMobileActive.svg
+++ b/app/icons/settingMobileActive.svg
@@ -1,9 +1,9 @@
-
-
+
+
-
-
-
+
+
+
-
\ No newline at end of file
+
diff --git a/app/icons/settingMobileInactive.svg b/app/icons/settingMobileInactive.svg
index b4df8064b..aae79566d 100644
--- a/app/icons/settingMobileInactive.svg
+++ b/app/icons/settingMobileInactive.svg
@@ -1,4 +1,4 @@
-
-
-
+
+
+
diff --git a/app/locales/cn.ts b/app/locales/cn.ts
index 3320bec71..2074b6d7b 100644
--- a/app/locales/cn.ts
+++ b/app/locales/cn.ts
@@ -128,7 +128,7 @@ const cn = {
Settings: {
Title: "设置",
SubTitle: "所有设置选项",
-
+ GeneralSettings: "通用设置",
Danger: {
Reset: {
Title: "重置所有设置",
diff --git a/app/locales/en.ts b/app/locales/en.ts
index 2b1c697ef..f7391658d 100644
--- a/app/locales/en.ts
+++ b/app/locales/en.ts
@@ -131,6 +131,7 @@ const en: LocaleType = {
Settings: {
Title: "Settings",
SubTitle: "All Settings",
+ GeneralSettings: "General settings",
Danger: {
Reset: {
Title: "Reset All Settings",
diff --git a/app/locales/jp.ts b/app/locales/jp.ts
index dcbd0f282..8f052eb31 100644
--- a/app/locales/jp.ts
+++ b/app/locales/jp.ts
@@ -95,6 +95,7 @@ const jp: PartialLocaleType = {
Settings: {
Title: "設定",
SubTitle: "設定オプション",
+ GeneralSettings: "一般設定",
Danger: {
Reset: {
Title: "設定をリセット",
diff --git a/app/store/chat.ts b/app/store/chat.ts
index eeddd8463..6fc2eee59 100644
--- a/app/store/chat.ts
+++ b/app/store/chat.ts
@@ -30,9 +30,26 @@ export type ChatMessage = RequestMessage & {
model?: ModelType;
};
-export function createMessage(override: Partial): ChatMessage {
+let tempGlobalId = 0;
+
+export function createMessage(
+ override: Partial,
+ options?: { temp?: boolean; customId?: string },
+): ChatMessage {
+ const { temp, customId } = options ?? {};
+
+ let id: string;
+ if (customId) {
+ id = customId;
+ } else if (temp) {
+ tempGlobalId += 1;
+ id = String(tempGlobalId);
+ } else {
+ id = nanoid();
+ }
+
return {
- id: nanoid(),
+ id,
date: new Date().toLocaleString(),
role: "user",
content: "",
diff --git a/app/styles/globals.css b/app/styles/globals.css
index 3b9bb97d4..037008e28 100644
--- a/app/styles/globals.css
+++ b/app/styles/globals.css
@@ -27,4 +27,5 @@ body {
:root {
--tip-popover-color: #434360;
--chat-panel-bg: rgb(249, 250, 251, 1);
+ --siderbar-mobile-height: 3.125rem;
}
diff --git a/tailwind.config.js b/tailwind.config.js
index a2c6db11b..ab82d1535 100644
--- a/tailwind.config.js
+++ b/tailwind.config.js
@@ -11,6 +11,11 @@ module.exports = {
'sm-mobile-tab': '0.625rem',
'chat-header-title': '1rem',
'actions-popover-menu-item': '15px',
+ 'setting-title': '1.25rem',
+ 'setting-items': '1rem',
+ },
+ fontWeight: {
+ 'setting-title': '700',
},
fontFamily: {
'common': ['Satoshi Variable', 'Variable'],
@@ -23,10 +28,6 @@ module.exports = {
xl: '1440px',
'2xl': '1980px'
},
- // spacing: Array.from({ length: 1000 }).reduce((map, _, index) => {
- // map[index] = `${index}rem`;
- // return map;
- // }, {}),
extend: {
minHeight: {
'chat-input-mobile': '19px',
@@ -41,11 +42,14 @@ module.exports = {
'actions-popover': '203px',
},
height: {
- mobile: '3.125rem',
+ mobile: 'var(--siderbar-mobile-height)',
+ // mobile: '3.125rem',
'menu-title-mobile': '3rem',
'thumbnail': '5rem',
'chat-input-mobile': '19px',
'chat-input': '60px',
+ 'chat-panel-mobile': '- var(--siderbar-mobile-height)',
+ 'setting-panel-mobile': 'calc(100vh - var(--siderbar-mobile-height))',
},
flexBasis: {
'sidebar': 'var(--sidebar-width)',
@@ -53,6 +57,7 @@ module.exports = {
},
spacing: {
'chat-header-gap': '0.625rem',
+ 'chat-panel-mobile': 'var(--siderbar-mobile-height)'
},
backgroundImage: {
'message-bg': 'linear-gradient(259deg, #9786FF 8.42%, #4A5CFF 90.13%)',
@@ -75,6 +80,7 @@ module.exports = {
'chat-input': '0px 4px 20px 0px rgba(60, 68, 255, 0.13)',
'actions-popover': '0px 14px 40px 0px rgba(0, 0, 0, 0.12)',
'actions-bar': '0px 4px 30px 0px rgba(0, 0, 0, 0.10)',
+ 'prompt-hint-container': 'inset 0 4px 8px 0 rgba(0, 0, 0, 0.1)'
}
},
borderRadius: {