From 7e8973c9ffba853b46ea9d795b1a05e81828ed37 Mon Sep 17 00:00:00 2001 From: Yidadaa Date: Thu, 20 Apr 2023 22:52:14 +0800 Subject: [PATCH 1/9] feat: close #291 gpt-4 model uses black icon --- app/components/chat.tsx | 12 +++++++++--- app/icons/black-bot.svg | 28 ++++++++++++++++++++++++++++ app/store/app.ts | 8 ++++++-- 3 files changed, 43 insertions(+), 5 deletions(-) create mode 100644 app/icons/black-bot.svg diff --git a/app/components/chat.tsx b/app/components/chat.tsx index 5dce8fd6..d27c138e 100644 --- a/app/components/chat.tsx +++ b/app/components/chat.tsx @@ -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,6 +31,7 @@ import { createMessage, useAccessStore, Theme, + ModelType, } from "../store"; import { @@ -64,13 +66,17 @@ const Emoji = dynamic(async () => (await import("emoji-picker-react")).Emoji, { loading: () => , }); -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 (
- + {props.model?.startsWith("gpt-4") ? ( + + ) : ( + + )}
); } @@ -727,7 +733,7 @@ export function Chat(props: { >
- +
{(message.preview || message.streaming) && (
diff --git a/app/icons/black-bot.svg b/app/icons/black-bot.svg new file mode 100644 index 00000000..3aad2add --- /dev/null +++ b/app/icons/black-bot.svg @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/store/app.ts b/app/store/app.ts index 8d875fee..5af4374c 100644 --- a/app/store/app.ts +++ b/app/store/app.ts @@ -17,6 +17,7 @@ export type Message = ChatCompletionResponseMessage & { streaming?: boolean; isError?: boolean; id?: number; + model?: ModelType; }; export function createMessage(override: Partial): 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, @@ -387,6 +390,7 @@ export const useChatStore = create()( role: "assistant", streaming: true, id: userMessage.id! + 1, + model: get().config.modelConfig.model, }); // get recent messages From e3d2dd72794aa3d2b63c477231d54b0df62111e6 Mon Sep 17 00:00:00 2001 From: Yidadaa Date: Thu, 20 Apr 2023 22:55:14 +0800 Subject: [PATCH 2/9] feat: close #427 add OPENAI_ORG_ID --- README.md | 4 ++++ README_CN.md | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/README.md b/README.md index 1764f2db..e9ec7e68 100644 --- a/README.md +++ b/README.md @@ -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#开发) diff --git a/README_CN.md b/README_CN.md index d2d64aa0..03ec2a10 100644 --- a/README_CN.md +++ b/README_CN.md @@ -94,6 +94,10 @@ OpenAI 接口代理 URL,如果你手动配置了 openai 接口代理,请填 > 如果遇到 ssl 证书问题,请将 `BASE_URL` 的协议设置为 http。 +### `OPENAI_ORG_ID` (可选) + +指定 OpenAI 中的组织 ID。 + ## 开发 > 强烈不建议在本地进行开发或者部署,由于一些技术原因,很难在本地配置好 OpenAI API 代理,除非你能保证可以直连 OpenAI 服务器。 From 2e9e69d66c56a0d0a2468b6456477d156980126b Mon Sep 17 00:00:00 2001 From: Yidadaa Date: Thu, 20 Apr 2023 22:58:19 +0800 Subject: [PATCH 3/9] fixup --- app/store/app.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/store/app.ts b/app/store/app.ts index 5af4374c..0a3ff865 100644 --- a/app/store/app.ts +++ b/app/store/app.ts @@ -122,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); From 06d503152bcba1ad9175441709d7e5c3044eea0a Mon Sep 17 00:00:00 2001 From: Yidadaa Date: Thu, 20 Apr 2023 23:04:58 +0800 Subject: [PATCH 4/9] feat: close #928 summarize with gpt-3.5 --- app/requests.ts | 38 ++++++++++++++++++++++++++++++++------ app/store/app.ts | 16 ++++++++-------- 2 files changed, 40 insertions(+), 14 deletions(-) diff --git a/app/requests.ts b/app/requests.ts index 3cb838e6..9159f1cf 100644 --- a/app/requests.ts +++ b/app/requests.ts @@ -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 ?? ""; } diff --git a/app/store/app.ts b/app/store/app.ts index 0a3ff865..fe2a07da 100644 --- a/app/store/app.ts +++ b/app/store/app.ts @@ -535,14 +535,14 @@ export const useChatStore = create()( 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; From 2390da11651c80bd3e0fd3935063614a5694aa02 Mon Sep 17 00:00:00 2001 From: Yidadaa Date: Thu, 20 Apr 2023 23:09:42 +0800 Subject: [PATCH 5/9] fix: #930 wont show delete for first message --- app/components/chat.tsx | 66 ++++++++++++++++++++++------------------- 1 file changed, 35 insertions(+), 31 deletions(-) diff --git a/app/components/chat.tsx b/app/components/chat.tsx index d27c138e..02f46141 100644 --- a/app/components/chat.tsx +++ b/app/components/chat.tsx @@ -723,6 +723,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 (
- {(message.preview || message.streaming) && ( + {showTyping && (
{Locale.Chat.Typing}
)}
- {!isUser && - !(message.preview || message.content.length === 0) && ( -
- {message.streaming ? ( -
onUserStop(message.id ?? i)} - > - {Locale.Chat.Actions.Stop} -
- ) : ( - <> -
onDelete(message.id ?? i)} - > - {Locale.Chat.Actions.Delete} -
-
onResend(message.id ?? i)} - > - {Locale.Chat.Actions.Retry} -
- - )} - + {showActions && ( +
+ {message.streaming ? (
copyToClipboard(message.content)} + onClick={() => onUserStop(message.id ?? i)} > - {Locale.Chat.Actions.Copy} + {Locale.Chat.Actions.Stop}
+ ) : ( + <> +
onDelete(message.id ?? i)} + > + {Locale.Chat.Actions.Delete} +
+
onResend(message.id ?? i)} + > + {Locale.Chat.Actions.Retry} +
+ + )} + +
copyToClipboard(message.content)} + > + {Locale.Chat.Actions.Copy}
- )} +
+ )} Date: Thu, 20 Apr 2023 23:20:25 +0800 Subject: [PATCH 6/9] feat: reactive isMobileScreen --- app/components/chat-list.tsx | 1 - app/components/chat.tsx | 13 +++++++------ app/components/home.tsx | 20 +++++++++----------- app/utils.ts | 18 ++++++++++++++++++ 4 files changed, 34 insertions(+), 18 deletions(-) diff --git a/app/components/chat-list.tsx b/app/components/chat-list.tsx index cab8812c..f0139205 100644 --- a/app/components/chat-list.tsx +++ b/app/components/chat-list.tsx @@ -10,7 +10,6 @@ import { import { useChatStore } from "../store"; import Locale from "../locales"; -import { isMobileScreen } from "../utils"; export function ChatItem(props: { onClick?: () => void; diff --git a/app/components/chat.tsx b/app/components/chat.tsx index 02f46141..c6bc61eb 100644 --- a/app/components/chat.tsx +++ b/app/components/chat.tsx @@ -38,9 +38,9 @@ import { copyToClipboard, downloadAs, getEmojiUrl, - isMobileScreen, selectOrCopy, autoGrowTextArea, + useMobileScreen, } from "../utils"; import dynamic from "next/dynamic"; @@ -438,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; @@ -468,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); }, @@ -508,7 +509,7 @@ export function Chat(props: { setBeforeInput(userInput); setUserInput(""); setPromptHints([]); - if (!isMobileScreen()) inputRef.current?.focus(); + if (!isMobileScreen) inputRef.current?.focus(); setAutoScroll(true); }; @@ -640,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 }, []); @@ -688,7 +689,7 @@ export function Chat(props: { }} />
- {!isMobileScreen() && ( + {!isMobileScreen && (
: } @@ -788,7 +789,7 @@ export function Chat(props: { } onContextMenu={(e) => onRightClick(e, message)} onDoubleClickCapture={() => { - if (!isMobileScreen()) return; + if (!isMobileScreen) return; setUserInput(message.content); }} fontSize={fontSize} diff --git a/app/components/home.tsx b/app/components/home.tsx index 828b7576..ef30243c 100644 --- a/app/components/home.tsx +++ b/app/components/home.tsx @@ -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 (
{ + const onResize = () => { + setIsMobileScreen(isMobileScreen()); + }; + + window.addEventListener("resize", onResize); + + return () => { + window.removeEventListener("resize", onResize); + }; + }, []); + + return isMobileScreen_; +} + export function isMobileScreen() { return window.innerWidth <= 600; } From 693dcf12d6c6ddd610b12bbc85ebab0474e46256 Mon Sep 17 00:00:00 2001 From: Yidadaa Date: Fri, 21 Apr 2023 01:12:39 +0800 Subject: [PATCH 7/9] refactor: close #643 use react router --- app/components/chat-list.tsx | 8 +- app/components/chat.tsx | 14 +-- app/components/home.tsx | 212 +++++++++-------------------------- app/components/settings.tsx | 10 +- app/components/sidebar.tsx | 135 ++++++++++++++++++++++ app/constant.ts | 6 + app/utils.ts | 2 +- package.json | 1 + yarn.lock | 20 ++++ 9 files changed, 234 insertions(+), 174 deletions(-) create mode 100644 app/components/sidebar.tsx diff --git a/app/components/chat-list.tsx b/app/components/chat-list.tsx index f0139205..fb0f740c 100644 --- a/app/components/chat-list.tsx +++ b/app/components/chat-list.tsx @@ -10,6 +10,8 @@ import { import { useChatStore } from "../store"; import Locale from "../locales"; +import { Link, useNavigate } from "react-router-dom"; +import { Path } from "../constant"; export function ChatItem(props: { onClick?: () => void; @@ -59,6 +61,7 @@ export function ChatList() { state.moveSession, ]); const chatStore = useChatStore(); + const navigate = useNavigate(); const onDragEnd: OnDragEndResponder = (result) => { const { destination, source } = result; @@ -94,7 +97,10 @@ export function ChatList() { id={item.id} index={i} selected={i === selectedIndex} - onClick={() => selectSession(i)} + onClick={() => { + navigate(Path.Chat); + selectSession(i); + }} onDelete={() => chatStore.deleteSession(i)} /> ))} diff --git a/app/components/chat.tsx b/app/components/chat.tsx index c6bc61eb..bab42298 100644 --- a/app/components/chat.tsx +++ b/app/components/chat.tsx @@ -54,6 +54,8 @@ import styles from "./home.module.scss"; import chatStyle from "./chat.module.scss"; import { Input, Modal, showModal } from "./ui-lib"; +import { useNavigate } from "react-router-dom"; +import { Path } from "../constant"; const Markdown = dynamic( async () => memo((await import("./markdown")).Markdown), @@ -418,10 +420,7 @@ export function ChatActions(props: { ); } -export function Chat(props: { - showSideBar?: () => void; - sideBarShowing?: boolean; -}) { +export function Chat() { type RenderMessage = Message & { preview?: boolean }; const chatStore = useChatStore(); @@ -439,6 +438,7 @@ export function Chat(props: { const { scrollRef, setAutoScroll, scrollToBottom } = useScrollToBottom(); const [hitBottom, setHitBottom] = useState(false); const isMobileScreen = useMobileScreen(); + const navigate = useNavigate(); const onChatBodyScroll = (e: HTMLElement) => { const isTouchBottom = e.scrollTop + e.clientHeight >= e.scrollHeight - 20; @@ -641,7 +641,7 @@ export function Chat(props: { // Auto focus useEffect(() => { - if (props.sideBarShowing && isMobileScreen) return; + if (isMobileScreen) return; inputRef.current?.focus(); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); @@ -666,7 +666,7 @@ export function Chat(props: { icon={} bordered title={Locale.Chat.Actions.ChatList} - onClick={props?.showSideBar} + onClick={() => navigate(Path.Home)} />
@@ -830,7 +830,7 @@ export function Chat(props: { setAutoScroll(false); setTimeout(() => setPromptHints([]), 500); }} - autoFocus={!props?.sideBarShowing} + autoFocus rows={inputRows} /> +
{!props.noLogo && }
@@ -38,7 +38,7 @@ const Settings = dynamic(async () => (await import("./settings")).Settings, { loading: () => , }); -const ChatList = dynamic(async () => (await import("./chat-list")).ChatList, { +const SideBar = dynamic(async () => (await import("./sidebar")).SideBar, { loading: () => , }); @@ -73,50 +73,6 @@ function useSwitchTheme() { }, [config.theme]); } -function useDragSideBar() { - const limit = (x: number) => Math.min(500, Math.max(220, x)); - - const chatStore = useChatStore(); - const startX = useRef(0); - const startDragWidth = useRef(chatStore.config.sidebarWidth ?? 300); - const lastUpdateTime = useRef(Date.now()); - - const handleMouseMove = useRef((e: MouseEvent) => { - if (Date.now() < lastUpdateTime.current + 100) { - return; - } - lastUpdateTime.current = Date.now(); - const d = e.clientX - startX.current; - const nextWidth = limit(startDragWidth.current + d); - chatStore.updateConfig((config) => (config.sidebarWidth = nextWidth)); - }); - - const handleMouseUp = useRef(() => { - startDragWidth.current = chatStore.config.sidebarWidth ?? 300; - window.removeEventListener("mousemove", handleMouseMove.current); - window.removeEventListener("mouseup", handleMouseUp.current); - }); - - const onDragMouseDown = (e: MouseEvent) => { - startX.current = e.clientX; - - window.addEventListener("mousemove", handleMouseMove.current); - window.addEventListener("mouseup", handleMouseUp.current); - }; - const isMobileScreen = useMobileScreen(); - - useEffect(() => { - const sideBarWidth = isMobileScreen - ? "100vw" - : `${limit(chatStore.config.sidebarWidth ?? 300)}px`; - document.documentElement.style.setProperty("--sidebar-width", sideBarWidth); - }, [chatStore.config.sidebarWidth, isMobileScreen]); - - return { - onDragMouseDown, - }; -} - const useHasHydrated = () => { const [hasHydrated, setHasHydrated] = useState(false); @@ -127,130 +83,64 @@ const useHasHydrated = () => { return hasHydrated; }; -function _Home() { - const [createNewSession, currentIndex, removeSession] = useChatStore( - (state) => [ - state.newSession, - state.currentSessionIndex, - state.removeSession, - ], - ); - const chatStore = useChatStore(); - const loading = !useHasHydrated(); - const [showSideBar, setShowSideBar] = useState(true); - +function WideScreen() { // setting - const [openSettings, setOpenSettings] = useState(false); const config = useChatStore((state) => state.config); - // drag side bar - const { onDragMouseDown } = useDragSideBar(); - const isMobileScreen = useMobileScreen(); - - useSwitchTheme(); - - if (loading) { - return ; - } - return (
-
-
-
ChatGPT Next
-
- Build your own AI assistant. -
-
- -
-
- -
{ - setOpenSettings(false); - setShowSideBar(false); - }} - > - -
- -
-
-
- } - onClick={chatStore.deleteSession} - /> -
-
- } - onClick={() => { - setOpenSettings(true); - setShowSideBar(false); - }} - shadow - /> -
- -
-
- } - text={Locale.Home.NewChat} - onClick={() => { - createNewSession(); - setShowSideBar(false); - }} - shadow - /> -
-
- -
onDragMouseDown(e as any)} - >
+
+
- {openSettings ? ( - { - setOpenSettings(false); - setShowSideBar(true); - }} - /> - ) : ( - setShowSideBar(true)} - sideBarShowing={showSideBar} - /> - )} + + } /> + } /> + } /> + +
+
+ ); +} + +function MobileScreen() { + const location = useLocation(); + const isHome = location.pathname === Path.Home; + + return ( +
+
+ +
+ +
+ + + } /> + } /> +
); } export function Home() { + useSwitchTheme(); + + const isMobileScreen = useMobileScreen(); + + if (!useHasHydrated()) { + return ; + } + return ( - <_Home> + {isMobileScreen ? : } ); } diff --git a/app/components/settings.tsx b/app/components/settings.tsx index d81b5b35..02148492 100644 --- a/app/components/settings.tsx +++ b/app/components/settings.tsx @@ -29,10 +29,11 @@ import { Avatar } from "./chat"; import Locale, { AllLangs, changeLang, getLang } from "../locales"; import { copyToClipboard, getEmojiUrl } from "../utils"; import Link from "next/link"; -import { UPDATE_URL } from "../constant"; +import { Path, UPDATE_URL } from "../constant"; import { Prompt, SearchService, usePromptStore } from "../store/prompt"; import { ErrorBoundary } from "./error"; import { InputRange } from "./input-range"; +import { useNavigate } from "react-router-dom"; function UserPromptModal(props: { onClose?: () => void }) { const promptStore = usePromptStore(); @@ -176,7 +177,8 @@ function PasswordInput(props: HTMLProps) { ); } -export function Settings(props: { closeSettings: () => void }) { +export function Settings() { + const navigate = useNavigate(); const [showEmojiPicker, setShowEmojiPicker] = useState(false); const [config, updateConfig, resetConfig, clearAllData, clearSessions] = useChatStore((state) => [ @@ -235,7 +237,7 @@ export function Settings(props: { closeSettings: () => void }) { useEffect(() => { const keydownEvent = (e: KeyboardEvent) => { if (e.key === "Escape") { - props.closeSettings(); + navigate(Path.Home); } }; document.addEventListener("keydown", keydownEvent); @@ -290,7 +292,7 @@ export function Settings(props: { closeSettings: () => void }) {
} - onClick={props.closeSettings} + onClick={() => navigate(Path.Home)} bordered title={Locale.Settings.Actions.Close} /> diff --git a/app/components/sidebar.tsx b/app/components/sidebar.tsx new file mode 100644 index 00000000..338dec19 --- /dev/null +++ b/app/components/sidebar.tsx @@ -0,0 +1,135 @@ +import { useState, useEffect, useRef } from "react"; + +import styles from "./home.module.scss"; + +import { IconButton } from "./button"; +import SettingsIcon from "../icons/settings.svg"; +import GithubIcon from "../icons/github.svg"; +import ChatGptIcon from "../icons/chatgpt.svg"; +import AddIcon from "../icons/add.svg"; +import CloseIcon from "../icons/close.svg"; +import Locale from "../locales"; + +import { useChatStore } from "../store"; + +import { Path, REPO_URL } from "../constant"; + +import { HashRouter as Router, Link, useNavigate } from "react-router-dom"; +import { useMobileScreen } from "../utils"; +import { ChatList } from "./chat-list"; + +function useDragSideBar() { + const limit = (x: number) => Math.min(500, Math.max(220, x)); + + const chatStore = useChatStore(); + const startX = useRef(0); + const startDragWidth = useRef(chatStore.config.sidebarWidth ?? 300); + const lastUpdateTime = useRef(Date.now()); + + const handleMouseMove = useRef((e: MouseEvent) => { + if (Date.now() < lastUpdateTime.current + 100) { + return; + } + lastUpdateTime.current = Date.now(); + const d = e.clientX - startX.current; + const nextWidth = limit(startDragWidth.current + d); + chatStore.updateConfig((config) => (config.sidebarWidth = nextWidth)); + }); + + const handleMouseUp = useRef(() => { + startDragWidth.current = chatStore.config.sidebarWidth ?? 300; + window.removeEventListener("mousemove", handleMouseMove.current); + window.removeEventListener("mouseup", handleMouseUp.current); + }); + + const onDragMouseDown = (e: MouseEvent) => { + startX.current = e.clientX; + + window.addEventListener("mousemove", handleMouseMove.current); + window.addEventListener("mouseup", handleMouseUp.current); + }; + const isMobileScreen = useMobileScreen(); + + useEffect(() => { + const sideBarWidth = isMobileScreen + ? "100vw" + : `${limit(chatStore.config.sidebarWidth ?? 300)}px`; + document.documentElement.style.setProperty("--sidebar-width", sideBarWidth); + }, [chatStore.config.sidebarWidth, isMobileScreen]); + + return { + onDragMouseDown, + }; +} + +export function SideBar(props: { setShowSideBar?: (_: boolean) => void }) { + const chatStore = useChatStore(); + + // drag side bar + const { onDragMouseDown } = useDragSideBar(); + const navigate = useNavigate(); + const isMobileScreen = useMobileScreen(); + + return ( + <> +
+
ChatGPT Next
+
+ Build your own AI assistant. +
+
+ +
+
+ +
{ + if (e.target === e.currentTarget) { + navigate(Path.Home); + } + props.setShowSideBar?.(false); + }} + > + +
+ +
+
+
+ } + onClick={chatStore.deleteSession} + /> +
+
+ + } shadow /> + +
+ +
+
+ } + text={Locale.Home.NewChat} + onClick={() => { + chatStore.newSession(); + props.setShowSideBar?.(false); + }} + shadow + /> +
+
+ +
onDragMouseDown(e as any)} + >
+ + ); +} diff --git a/app/constant.ts b/app/constant.ts index 6f08ad75..68744546 100644 --- a/app/constant.ts +++ b/app/constant.ts @@ -6,3 +6,9 @@ export const UPDATE_URL = `${REPO_URL}#keep-updated`; export const FETCH_COMMIT_URL = `https://api.github.com/repos/${OWNER}/${REPO}/commits?per_page=1`; export const FETCH_TAG_URL = `https://api.github.com/repos/${OWNER}/${REPO}/tags?per_page=1`; export const RUNTIME_CONFIG_DOM = "danger-runtime-config"; + +export enum Path { + Home = "/", + Chat = "/chat", + Settings = "/settings", +} diff --git a/app/utils.ts b/app/utils.ts index 9d6e9062..0af9fef5 100644 --- a/app/utils.ts +++ b/app/utils.ts @@ -49,7 +49,7 @@ export function isIOS() { } export function useMobileScreen() { - const [isMobileScreen_, setIsMobileScreen] = useState(false); + const [isMobileScreen_, setIsMobileScreen] = useState(isMobileScreen()); useEffect(() => { const onResize = () => { setIsMobileScreen(isMobileScreen()); diff --git a/package.json b/package.json index 19047ad1..78500551 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,7 @@ "react": "^18.2.0", "react-dom": "^18.2.0", "react-markdown": "^8.0.5", + "react-router-dom": "^6.10.0", "rehype-highlight": "^6.0.0", "rehype-katex": "^6.0.2", "remark-breaks": "^3.0.2", diff --git a/yarn.lock b/yarn.lock index 342ea4a4..b7d9f830 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1189,6 +1189,11 @@ tiny-glob "^0.2.9" tslib "^2.4.0" +"@remix-run/router@1.5.0": + version "1.5.0" + resolved "https://registry.npmmirror.com/@remix-run/router/-/router-1.5.0.tgz#57618e57942a5f0131374a9fdb0167e25a117fdc" + integrity sha512-bkUDCp8o1MvFO+qxkODcbhSqRa6P2GXgrGZVpt0dCXNW2HCSCqYI0ZoAqEOSAjRWmmlKcYgFvN4B4S+zo/f8kg== + "@rushstack/eslint-patch@^1.1.3": version "1.2.0" resolved "https://registry.yarnpkg.com/@rushstack/eslint-patch/-/eslint-patch-1.2.0.tgz#8be36a1f66f3265389e90b5f9c9962146758f728" @@ -4296,6 +4301,21 @@ react-redux@^8.0.4: react-is "^18.0.0" use-sync-external-store "^1.0.0" +react-router-dom@^6.10.0: + version "6.10.0" + resolved "https://registry.npmmirror.com/react-router-dom/-/react-router-dom-6.10.0.tgz#090ddc5c84dc41b583ce08468c4007c84245f61f" + integrity sha512-E5dfxRPuXKJqzwSe/qGcqdwa18QiWC6f3H3cWXM24qj4N0/beCIf/CWTipop2xm7mR0RCS99NnaqPNjHtrAzCg== + dependencies: + "@remix-run/router" "1.5.0" + react-router "6.10.0" + +react-router@6.10.0: + version "6.10.0" + resolved "https://registry.npmmirror.com/react-router/-/react-router-6.10.0.tgz#230f824fde9dd0270781b5cb497912de32c0a971" + integrity sha512-Nrg0BWpQqrC3ZFFkyewrflCud9dio9ME3ojHCF/WLsprJVzkq3q3UeEhMCAW1dobjeGbWgjNn/PVF6m46ANxXQ== + dependencies: + "@remix-run/router" "1.5.0" + react@^18.2.0: version "18.2.0" resolved "https://registry.yarnpkg.com/react/-/react-18.2.0.tgz#555bd98592883255fa00de14f1151a917b5d77d5" From 5185166e3b5b1f2b802833c79565c51c766b912e Mon Sep 17 00:00:00 2001 From: Yidadaa Date: Fri, 21 Apr 2023 01:18:49 +0800 Subject: [PATCH 8/9] fixup --- app/components/home.tsx | 2 -- app/utils.ts | 3 +++ 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/app/components/home.tsx b/app/components/home.tsx index efca5c0b..8b5e1d74 100644 --- a/app/components/home.tsx +++ b/app/components/home.tsx @@ -21,7 +21,6 @@ import { HashRouter as Router, Routes, Route, - useNavigation, useLocation, } from "react-router-dom"; @@ -131,7 +130,6 @@ function MobileScreen() { export function Home() { useSwitchTheme(); - const isMobileScreen = useMobileScreen(); if (!useHasHydrated()) { diff --git a/app/utils.ts b/app/utils.ts index 0af9fef5..dfec8d3e 100644 --- a/app/utils.ts +++ b/app/utils.ts @@ -66,6 +66,9 @@ export function useMobileScreen() { } export function isMobileScreen() { + if (typeof window === "undefined") { + return false; + } return window.innerWidth <= 600; } From 82ad0573be93b0ee43f9cc52b865ea8878988dfa Mon Sep 17 00:00:00 2001 From: Yidadaa Date: Fri, 21 Apr 2023 02:52:53 +0800 Subject: [PATCH 9/9] feat: close #380 collapse side bar --- app/components/chat-list.tsx | 31 ++++++++++----- app/components/home.module.scss | 69 ++++++++++++++++++++++++++++++++- app/components/home.tsx | 13 ++----- app/components/sidebar.tsx | 43 ++++++++++++-------- app/constant.ts | 4 ++ 5 files changed, 125 insertions(+), 35 deletions(-) diff --git a/app/components/chat-list.tsx b/app/components/chat-list.tsx index fb0f740c..626336af 100644 --- a/app/components/chat-list.tsx +++ b/app/components/chat-list.tsx @@ -22,6 +22,7 @@ export function ChatItem(props: { selected: boolean; id: number; index: number; + narrow?: boolean; }) { return ( @@ -35,13 +36,20 @@ export function ChatItem(props: { {...provided.draggableProps} {...provided.dragHandleProps} > -
{props.title}
-
-
- {Locale.ChatItem.ChatItemCount(props.count)} -
-
{props.time}
-
+ {props.narrow ? ( +
{props.count}
+ ) : ( + <> +
{props.title}
+
+
+ {Locale.ChatItem.ChatItemCount(props.count)} +
+
{props.time}
+
+ + )} +
@@ -51,7 +59,7 @@ export function ChatItem(props: { ); } -export function ChatList() { +export function ChatList(props: { narrow?: boolean }) { const [sessions, selectedIndex, selectSession, removeSession, moveSession] = useChatStore((state) => [ state.sessions, @@ -101,7 +109,12 @@ export function ChatList() { navigate(Path.Chat); selectSession(i); }} - onDelete={() => chatStore.deleteSession(i)} + onDelete={() => { + if (!props.narrow || confirm(Locale.Home.DeleteChat)) { + chatStore.deleteSession(i); + } + }} + narrow={props.narrow} /> ))} {provided.placeholder} diff --git a/app/components/home.module.scss b/app/components/home.module.scss index 9bf0d571..38e755bc 100644 --- a/app/components/home.module.scss +++ b/app/components/home.module.scss @@ -50,7 +50,7 @@ flex-direction: column; box-shadow: inset -2px 0px 2px 0px rgb(0, 0, 0, 0.05); position: relative; - transition: width ease 0.1s; + transition: width ease 0.05s; } .sidebar-drag { @@ -126,11 +126,13 @@ .sidebar-title { font-size: 20px; font-weight: bold; + animation: slide-in ease 0.3s; } .sidebar-sub-title { font-size: 12px; font-weight: 400px; + animation: slide-in ease 0.3s; } .sidebar-body { @@ -171,6 +173,7 @@ overflow: hidden; text-overflow: ellipsis; white-space: nowrap; + animation: slide-in ease 0.3s; } .chat-item-delete { @@ -197,6 +200,7 @@ color: rgb(166, 166, 166); font-size: 12px; margin-top: 8px; + animation: slide-in ease 0.3s; } .chat-item-count, @@ -206,6 +210,69 @@ white-space: nowrap; } +.narrow-sidebar { + .sidebar-title, + .sidebar-sub-title { + display: none; + } + .sidebar-logo { + position: relative; + display: flex; + justify-content: center; + } + + .chat-item { + padding: 0; + min-height: 50px; + display: flex; + justify-content: center; + align-items: center; + transition: all ease 0.3s; + + &:hover { + .chat-item-narrow { + transform: scale(0.7) translateX(-50%); + } + } + } + + .chat-item-narrow { + font-weight: bolder; + font-size: 24px; + line-height: 0; + font-weight: lighter; + color: var(--black); + transform: translateX(0); + transition: all ease 0.3s; + opacity: 0.1; + padding: 4px; + } + + .chat-item-delete { + top: 15px; + } + + .chat-item:hover > .chat-item-delete { + opacity: 0.5; + right: 5px; + } + + .sidebar-tail { + flex-direction: column; + align-items: center; + + .sidebar-actions { + flex-direction: column; + align-items: center; + + .sidebar-action { + margin-right: 0; + margin-bottom: 15px; + } + } + } +} + .sidebar-tail { display: flex; justify-content: space-between; diff --git a/app/components/home.tsx b/app/components/home.tsx index 8b5e1d74..123be03a 100644 --- a/app/components/home.tsx +++ b/app/components/home.tsx @@ -41,7 +41,7 @@ const SideBar = dynamic(async () => (await import("./sidebar")).SideBar, { loading: () => , }); -function useSwitchTheme() { +export function useSwitchTheme() { const config = useChatStore((state) => state.config); useEffect(() => { @@ -83,7 +83,6 @@ const useHasHydrated = () => { }; function WideScreen() { - // setting const config = useChatStore((state) => state.config); return ( @@ -92,9 +91,7 @@ function WideScreen() { config.tightBorder ? styles["tight-container"] : styles.container }`} > -
- -
+
@@ -113,9 +110,7 @@ function MobileScreen() { return (
-
- -
+
@@ -129,8 +124,8 @@ function MobileScreen() { } export function Home() { - useSwitchTheme(); const isMobileScreen = useMobileScreen(); + useSwitchTheme(); if (!useHasHydrated()) { return ; diff --git a/app/components/sidebar.tsx b/app/components/sidebar.tsx index 338dec19..71e75f8a 100644 --- a/app/components/sidebar.tsx +++ b/app/components/sidebar.tsx @@ -12,14 +12,20 @@ import Locale from "../locales"; import { useChatStore } from "../store"; -import { Path, REPO_URL } from "../constant"; +import { + MAX_SIDEBAR_WIDTH, + MIN_SIDEBAR_WIDTH, + NARROW_SIDEBAR_WIDTH, + Path, + REPO_URL, +} from "../constant"; import { HashRouter as Router, Link, useNavigate } from "react-router-dom"; import { useMobileScreen } from "../utils"; import { ChatList } from "./chat-list"; function useDragSideBar() { - const limit = (x: number) => Math.min(500, Math.max(220, x)); + const limit = (x: number) => Math.min(MAX_SIDEBAR_WIDTH, x); const chatStore = useChatStore(); const startX = useRef(0); @@ -27,7 +33,7 @@ function useDragSideBar() { const lastUpdateTime = useRef(Date.now()); const handleMouseMove = useRef((e: MouseEvent) => { - if (Date.now() < lastUpdateTime.current + 100) { + if (Date.now() < lastUpdateTime.current + 50) { return; } lastUpdateTime.current = Date.now(); @@ -49,29 +55,36 @@ function useDragSideBar() { window.addEventListener("mouseup", handleMouseUp.current); }; const isMobileScreen = useMobileScreen(); + const shouldNarrow = + !isMobileScreen && chatStore.config.sidebarWidth < MIN_SIDEBAR_WIDTH; useEffect(() => { - const sideBarWidth = isMobileScreen - ? "100vw" - : `${limit(chatStore.config.sidebarWidth ?? 300)}px`; + const barWidth = shouldNarrow + ? NARROW_SIDEBAR_WIDTH + : limit(chatStore.config.sidebarWidth ?? 300); + const sideBarWidth = isMobileScreen ? "100vw" : `${barWidth}px`; document.documentElement.style.setProperty("--sidebar-width", sideBarWidth); - }, [chatStore.config.sidebarWidth, isMobileScreen]); + }, [chatStore.config.sidebarWidth, isMobileScreen, shouldNarrow]); return { onDragMouseDown, + shouldNarrow, }; } -export function SideBar(props: { setShowSideBar?: (_: boolean) => void }) { +export function SideBar(props: { className?: string }) { const chatStore = useChatStore(); // drag side bar - const { onDragMouseDown } = useDragSideBar(); + const { onDragMouseDown, shouldNarrow } = useDragSideBar(); const navigate = useNavigate(); - const isMobileScreen = useMobileScreen(); return ( - <> +
ChatGPT Next
@@ -88,10 +101,9 @@ export function SideBar(props: { setShowSideBar?: (_: boolean) => void }) { if (e.target === e.currentTarget) { navigate(Path.Home); } - props.setShowSideBar?.(false); }} > - +
@@ -116,10 +128,9 @@ export function SideBar(props: { setShowSideBar?: (_: boolean) => void }) {
} - text={Locale.Home.NewChat} + text={shouldNarrow ? undefined : Locale.Home.NewChat} onClick={() => { chatStore.newSession(); - props.setShowSideBar?.(false); }} shadow /> @@ -130,6 +141,6 @@ export function SideBar(props: { setShowSideBar?: (_: boolean) => void }) { className={styles["sidebar-drag"]} onMouseDown={(e) => onDragMouseDown(e as any)} >
- +
); } diff --git a/app/constant.ts b/app/constant.ts index 68744546..43ae4cc6 100644 --- a/app/constant.ts +++ b/app/constant.ts @@ -12,3 +12,7 @@ export enum Path { Chat = "/chat", Settings = "/settings", } + +export const MAX_SIDEBAR_WIDTH = 500; +export const MIN_SIDEBAR_WIDTH = 230; +export const NARROW_SIDEBAR_WIDTH = 100;