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 服务器。 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 5dce8fd6..c6bc61eb 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,15 +31,16 @@ import { createMessage, useAccessStore, Theme, + ModelType, } from "../store"; import { copyToClipboard, downloadAs, getEmojiUrl, - isMobileScreen, selectOrCopy, autoGrowTextArea, + useMobileScreen, } from "../utils"; import dynamic from "next/dynamic"; @@ -64,13 +66,17 @@ const Emoji = dynamic(async () => (await import("emoji-picker-react")).Emoji, { loading: () => , }); -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") ? ( + + ) : ( + + )}
); } @@ -432,6 +438,7 @@ export function Chat(props: { const { submitKey, shouldSubmit } = useSubmitHandler(); const { scrollRef, setAutoScroll, scrollToBottom } = useScrollToBottom(); const [hitBottom, setHitBottom] = useState(false); + const isMobileScreen = useMobileScreen(); const onChatBodyScroll = (e: HTMLElement) => { const isTouchBottom = e.scrollTop + e.clientHeight >= e.scrollHeight - 20; @@ -462,7 +469,7 @@ export function Chat(props: { const rows = inputRef.current ? autoGrowTextArea(inputRef.current) : 1; const inputRows = Math.min( 5, - Math.max(2 + Number(!isMobileScreen()), rows), + Math.max(2 + Number(!isMobileScreen), rows), ); setInputRows(inputRows); }, @@ -502,7 +509,7 @@ export function Chat(props: { setBeforeInput(userInput); setUserInput(""); setPromptHints([]); - if (!isMobileScreen()) inputRef.current?.focus(); + if (!isMobileScreen) inputRef.current?.focus(); setAutoScroll(true); }; @@ -634,7 +641,7 @@ export function Chat(props: { // Auto focus useEffect(() => { - if (props.sideBarShowing && isMobileScreen()) return; + if (props.sideBarShowing && isMobileScreen) return; inputRef.current?.focus(); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); @@ -682,7 +689,7 @@ export function Chat(props: { }} /> - {!isMobileScreen() && ( + {!isMobileScreen && (
: } @@ -717,6 +724,11 @@ export function Chat(props: { > {messages.map((message, i) => { const isUser = message.role === "user"; + const showActions = + !isUser && + i > 0 && + !(message.preview || message.content.length === 0); + const showTyping = message.preview || message.streaming; return (
- +
- {(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}
- )} +
+ )} 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 (
+ + + + + + + + + + + + + + + + + + + + + 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 8d875fee..fe2a07da 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, @@ -119,7 +122,7 @@ export function limitModel(name: string) { export const ModalConfigValidator = { model(x: string) { - return limitModel(x); + return limitModel(x) as ModelType; }, max_tokens(x: number) { return limitNumber(x, 0, 32000, 2000); @@ -387,6 +390,7 @@ export const useChatStore = create()( role: "assistant", streaming: true, id: userMessage.id! + 1, + model: get().config.modelConfig.model, }); // get recent messages @@ -531,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; diff --git a/app/utils.ts b/app/utils.ts index 0e4a8eae..9d6e9062 100644 --- a/app/utils.ts +++ b/app/utils.ts @@ -1,4 +1,5 @@ import { EmojiStyle } from "emoji-picker-react"; +import { useEffect, useState } from "react"; import { showToast } from "./components/ui-lib"; import Locale from "./locales"; @@ -47,6 +48,23 @@ export function isIOS() { return /iphone|ipad|ipod/.test(userAgent); } +export function useMobileScreen() { + const [isMobileScreen_, setIsMobileScreen] = useState(false); + useEffect(() => { + const onResize = () => { + setIsMobileScreen(isMobileScreen()); + }; + + window.addEventListener("resize", onResize); + + return () => { + window.removeEventListener("resize", onResize); + }; + }, []); + + return isMobileScreen_; +} + export function isMobileScreen() { return window.innerWidth <= 600; }