Merge branch 'main' b0c6f6d 20230616 into pro
This commit is contained in:
@@ -2,7 +2,6 @@ import { NextRequest } from "next/server";
|
||||
import { getServerSideConfig } from "../config/server";
|
||||
import md5 from "spark-md5";
|
||||
import { ACCESS_CODE_PREFIX } from "../constant";
|
||||
import { OPENAI_URL } from "./common";
|
||||
|
||||
function getIP(req: NextRequest) {
|
||||
let ip = req.ip ?? req.headers.get("x-real-ip");
|
||||
|
||||
@@ -1,14 +1,36 @@
|
||||
import { OpenaiPath } from "@/app/constant";
|
||||
import { prettyObject } from "@/app/utils/format";
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { auth } from "../../auth";
|
||||
import { requestOpenai } from "../../common";
|
||||
|
||||
const ALLOWD_PATH = new Set(Object.values(OpenaiPath));
|
||||
|
||||
async function handle(
|
||||
req: NextRequest,
|
||||
{ params }: { params: { path: string[] } },
|
||||
) {
|
||||
console.log("[OpenAI Route] params ", params);
|
||||
|
||||
if (req.method === "OPTIONS") {
|
||||
return NextResponse.json({ body: "OK" }, { status: 200 });
|
||||
}
|
||||
|
||||
const subpath = params.path.join("/");
|
||||
|
||||
if (!ALLOWD_PATH.has(subpath)) {
|
||||
console.log("[OpenAI Route] forbidden path ", subpath);
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: true,
|
||||
msg: "you are not allowed to request " + subpath,
|
||||
},
|
||||
{
|
||||
status: 403,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
const authResult = auth(req);
|
||||
if (authResult.error) {
|
||||
return NextResponse.json(authResult, {
|
||||
|
||||
+23
-1
@@ -1,5 +1,6 @@
|
||||
import { getClientConfig } from "../config/client";
|
||||
import { ACCESS_CODE_PREFIX } from "../constant";
|
||||
import { ChatMessage, ModelConfig, ModelType, useAccessStore } from "../store";
|
||||
import { ChatMessage, ModelType, useAccessStore } from "../store";
|
||||
import { ChatGPTApi } from "./platforms/openai";
|
||||
|
||||
export const ROLES = ["system", "user", "assistant"] as const;
|
||||
@@ -42,6 +43,27 @@ export abstract class LLMApi {
|
||||
abstract usage(): Promise<LLMUsage>;
|
||||
}
|
||||
|
||||
type ProviderName = "openai" | "azure" | "claude" | "palm";
|
||||
|
||||
interface Model {
|
||||
name: string;
|
||||
provider: ProviderName;
|
||||
ctxlen: number;
|
||||
}
|
||||
|
||||
interface ChatProvider {
|
||||
name: ProviderName;
|
||||
apiConfig: {
|
||||
baseUrl: string;
|
||||
apiKey: string;
|
||||
summaryModel: Model;
|
||||
};
|
||||
models: Model[];
|
||||
|
||||
chat: () => void;
|
||||
usage: () => void;
|
||||
}
|
||||
|
||||
export class ClientApi {
|
||||
public llm: LLMApi;
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { REQUEST_TIMEOUT_MS } from "@/app/constant";
|
||||
import { OpenaiPath, REQUEST_TIMEOUT_MS } from "@/app/constant";
|
||||
import { useAccessStore, useAppConfig, useChatStore } from "@/app/store";
|
||||
|
||||
import { ChatOptions, getHeaders, LLMApi, LLMUsage } from "../api";
|
||||
@@ -10,10 +10,6 @@ import {
|
||||
import { prettyObject } from "@/app/utils/format";
|
||||
|
||||
export class ChatGPTApi implements LLMApi {
|
||||
public ChatPath = "v1/chat/completions";
|
||||
public UsagePath = "dashboard/billing/usage";
|
||||
public SubsPath = "dashboard/billing/subscription";
|
||||
|
||||
path(path: string): string {
|
||||
let openaiUrl = useAccessStore.getState().openaiUrl;
|
||||
if (openaiUrl.endsWith("/")) {
|
||||
@@ -55,7 +51,7 @@ export class ChatGPTApi implements LLMApi {
|
||||
options.onController?.(controller);
|
||||
|
||||
try {
|
||||
const chatPath = this.path(this.ChatPath);
|
||||
const chatPath = this.path(OpenaiPath.ChatPath);
|
||||
const chatPayload = {
|
||||
method: "POST",
|
||||
body: JSON.stringify(requestPayload),
|
||||
@@ -177,14 +173,14 @@ export class ChatGPTApi implements LLMApi {
|
||||
const [used, subs] = await Promise.all([
|
||||
fetch(
|
||||
this.path(
|
||||
`${this.UsagePath}?start_date=${startDate}&end_date=${endDate}`,
|
||||
`${OpenaiPath.UsagePath}?start_date=${startDate}&end_date=${endDate}`,
|
||||
),
|
||||
{
|
||||
method: "GET",
|
||||
headers: getHeaders(),
|
||||
},
|
||||
),
|
||||
fetch(this.path(this.SubsPath), {
|
||||
fetch(this.path(OpenaiPath.SubsPath), {
|
||||
method: "GET",
|
||||
headers: getHeaders(),
|
||||
}),
|
||||
@@ -228,3 +224,4 @@ export class ChatGPTApi implements LLMApi {
|
||||
} as LLMUsage;
|
||||
}
|
||||
}
|
||||
export { OpenaiPath };
|
||||
|
||||
@@ -72,9 +72,7 @@ export function ChatItem(props: {
|
||||
<div className={styles["chat-item-count"]}>
|
||||
{Locale.ChatItem.ChatItemCount(props.count)}
|
||||
</div>
|
||||
<div className={styles["chat-item-date"]}>
|
||||
{new Date(props.time).toLocaleString()}
|
||||
</div>
|
||||
<div className={styles["chat-item-date"]}>{props.time}</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -17,10 +17,38 @@
|
||||
transition: all ease 0.3s;
|
||||
margin-bottom: 10px;
|
||||
align-items: center;
|
||||
height: 16px;
|
||||
width: var(--icon-width);
|
||||
|
||||
&:not(:last-child) {
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.text {
|
||||
white-space: nowrap;
|
||||
padding-left: 5px;
|
||||
opacity: 0;
|
||||
transform: translateX(-5px);
|
||||
transition: all ease 0.3s;
|
||||
transition-delay: 0.1s;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
width: var(--full-width);
|
||||
|
||||
.text {
|
||||
opacity: 1;
|
||||
transform: translate(0);
|
||||
}
|
||||
}
|
||||
|
||||
.text,
|
||||
.icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+102
-43
@@ -1,5 +1,11 @@
|
||||
import { useDebouncedCallback } from "use-debounce";
|
||||
import { useState, useRef, useEffect, useLayoutEffect } from "react";
|
||||
import React, {
|
||||
useState,
|
||||
useRef,
|
||||
useEffect,
|
||||
useLayoutEffect,
|
||||
useMemo,
|
||||
} from "react";
|
||||
|
||||
import SendWhiteIcon from "../icons/send-white.svg";
|
||||
import BrainIcon from "../icons/brain.svg";
|
||||
@@ -61,6 +67,7 @@ import { useMaskStore } from "../store/mask";
|
||||
import { useCommand } from "../command";
|
||||
import { prettyObject } from "../utils/format";
|
||||
import { ExportMessageModal } from "./exporter";
|
||||
import { getClientConfig } from "../config/client";
|
||||
|
||||
const Markdown = dynamic(async () => (await import("./markdown")).Markdown, {
|
||||
loading: () => <LoadingIcon />,
|
||||
@@ -279,6 +286,57 @@ function ClearContextDivider() {
|
||||
);
|
||||
}
|
||||
|
||||
function ChatAction(props: {
|
||||
text: string;
|
||||
icon: JSX.Element;
|
||||
onClick: () => void;
|
||||
}) {
|
||||
const iconRef = useRef<HTMLDivElement>(null);
|
||||
const textRef = useRef<HTMLDivElement>(null);
|
||||
const [width, setWidth] = useState({
|
||||
full: 20,
|
||||
icon: 20,
|
||||
});
|
||||
|
||||
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,
|
||||
});
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
updateWidth();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`${chatStyle["chat-input-action"]} clickable`}
|
||||
onClick={() => {
|
||||
props.onClick();
|
||||
setTimeout(updateWidth, 1);
|
||||
}}
|
||||
style={
|
||||
{
|
||||
"--icon-width": `${width.icon}px`,
|
||||
"--full-width": `${width.full}px`,
|
||||
} as React.CSSProperties
|
||||
}
|
||||
>
|
||||
<div ref={iconRef} className={chatStyle["icon"]}>
|
||||
{props.icon}
|
||||
</div>
|
||||
<div className={chatStyle["text"]} ref={textRef}>
|
||||
{props.text}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function useScrollToBottom() {
|
||||
// for auto-scroll
|
||||
const scrollRef = useRef<HTMLDivElement>(null);
|
||||
@@ -330,61 +388,60 @@ export function ChatActions(props: {
|
||||
return (
|
||||
<div className={chatStyle["chat-input-actions"]}>
|
||||
{couldStop && (
|
||||
<div
|
||||
className={`${chatStyle["chat-input-action"]} clickable`}
|
||||
<ChatAction
|
||||
onClick={stopAll}
|
||||
>
|
||||
<StopIcon />
|
||||
</div>
|
||||
text={Locale.Chat.InputActions.Stop}
|
||||
icon={<StopIcon />}
|
||||
/>
|
||||
)}
|
||||
{!props.hitBottom && (
|
||||
<div
|
||||
className={`${chatStyle["chat-input-action"]} clickable`}
|
||||
<ChatAction
|
||||
onClick={props.scrollToBottom}
|
||||
>
|
||||
<BottomIcon />
|
||||
</div>
|
||||
text={Locale.Chat.InputActions.ToBottom}
|
||||
icon={<BottomIcon />}
|
||||
/>
|
||||
)}
|
||||
{props.hitBottom && (
|
||||
<div
|
||||
className={`${chatStyle["chat-input-action"]} clickable`}
|
||||
<ChatAction
|
||||
onClick={props.showPromptModal}
|
||||
>
|
||||
<SettingsIcon />
|
||||
</div>
|
||||
text={Locale.Chat.InputActions.Settings}
|
||||
icon={<SettingsIcon />}
|
||||
/>
|
||||
)}
|
||||
|
||||
<div
|
||||
className={`${chatStyle["chat-input-action"]} clickable`}
|
||||
<ChatAction
|
||||
onClick={nextTheme}
|
||||
>
|
||||
{theme === Theme.Auto ? (
|
||||
<AutoIcon />
|
||||
) : theme === Theme.Light ? (
|
||||
<LightIcon />
|
||||
) : theme === Theme.Dark ? (
|
||||
<DarkIcon />
|
||||
) : null}
|
||||
</div>
|
||||
text={Locale.Chat.InputActions.Theme[theme]}
|
||||
icon={
|
||||
<>
|
||||
{theme === Theme.Auto ? (
|
||||
<AutoIcon />
|
||||
) : theme === Theme.Light ? (
|
||||
<LightIcon />
|
||||
) : theme === Theme.Dark ? (
|
||||
<DarkIcon />
|
||||
) : null}
|
||||
</>
|
||||
}
|
||||
/>
|
||||
|
||||
<div
|
||||
className={`${chatStyle["chat-input-action"]} clickable`}
|
||||
<ChatAction
|
||||
onClick={props.showPromptHints}
|
||||
>
|
||||
<PromptIcon />
|
||||
</div>
|
||||
text={Locale.Chat.InputActions.Prompt}
|
||||
icon={<PromptIcon />}
|
||||
/>
|
||||
|
||||
<div
|
||||
className={`${chatStyle["chat-input-action"]} clickable`}
|
||||
<ChatAction
|
||||
onClick={() => {
|
||||
navigate(Path.Masks);
|
||||
}}
|
||||
>
|
||||
<MaskIcon />
|
||||
</div>
|
||||
text={Locale.Chat.InputActions.Masks}
|
||||
icon={<MaskIcon />}
|
||||
/>
|
||||
|
||||
<div
|
||||
className={`${chatStyle["chat-input-action"]} clickable`}
|
||||
<ChatAction
|
||||
text={Locale.Chat.InputActions.Clear}
|
||||
icon={<BreakIcon />}
|
||||
onClick={() => {
|
||||
chatStore.updateCurrentSession((session) => {
|
||||
if (session.clearContextIndex === session.messages.length) {
|
||||
@@ -395,9 +452,7 @@ export function ChatActions(props: {
|
||||
}
|
||||
});
|
||||
}}
|
||||
>
|
||||
<ClearIcon />
|
||||
</div>
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -656,9 +711,13 @@ export function Chat() {
|
||||
}
|
||||
};
|
||||
|
||||
const clientConfig = useMemo(() => getClientConfig(), []);
|
||||
|
||||
const location = useLocation();
|
||||
const isChat = location.pathname === Path.Chat;
|
||||
|
||||
const autoFocus = !isMobileScreen || isChat; // only focus in chat page
|
||||
const showMaxIcon = !isMobileScreen && !clientConfig?.isApp;
|
||||
|
||||
useCommand({
|
||||
fill: setUserInput,
|
||||
@@ -707,7 +766,7 @@ export function Chat() {
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{!isMobileScreen && (
|
||||
{showMaxIcon && (
|
||||
<div className="window-action-button">
|
||||
<IconButton
|
||||
icon={config.tightBorder ? <MinIcon /> : <MaxIcon />}
|
||||
|
||||
+11
-1
@@ -24,6 +24,7 @@ import {
|
||||
import { SideBar } from "./sidebar";
|
||||
import { useAppConfig } from "../store/config";
|
||||
import { AuthPage } from "./auth";
|
||||
import { getClientConfig } from "../config/client";
|
||||
|
||||
export function Loading(props: { noLogo?: boolean }) {
|
||||
return (
|
||||
@@ -93,9 +94,14 @@ const useHasHydrated = () => {
|
||||
|
||||
const loadAsyncGoogleFont = () => {
|
||||
const linkEl = document.createElement("link");
|
||||
const proxyFontUrl = "/google-fonts";
|
||||
const remoteFontUrl = "https://fonts.googleapis.com";
|
||||
const googleFontUrl =
|
||||
getClientConfig()?.buildMode === "export" ? remoteFontUrl : proxyFontUrl;
|
||||
linkEl.rel = "stylesheet";
|
||||
linkEl.href =
|
||||
"/google-fonts/css2?family=Noto+Sans+SC:wght@300;400;700;900&display=swap";
|
||||
googleFontUrl +
|
||||
"/css2?family=Noto+Sans+SC:wght@300;400;700;900&display=swap";
|
||||
document.head.appendChild(linkEl);
|
||||
};
|
||||
|
||||
@@ -147,6 +153,10 @@ function Screen() {
|
||||
export function Home() {
|
||||
useSwitchTheme();
|
||||
|
||||
useEffect(() => {
|
||||
console.log("[Config] got config from build time", getClientConfig());
|
||||
}, []);
|
||||
|
||||
if (!useHasHydrated()) {
|
||||
return <Loading />;
|
||||
}
|
||||
|
||||
+42
-27
@@ -11,6 +11,7 @@ import mermaid from "mermaid";
|
||||
|
||||
import LoadingIcon from "../icons/three-dots.svg";
|
||||
import React from "react";
|
||||
import { useThrottledCallback } from "use-debounce";
|
||||
|
||||
export function Mermaid(props: { code: string; onError: () => void }) {
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
@@ -127,43 +128,57 @@ export function Markdown(
|
||||
) {
|
||||
const mdRef = useRef<HTMLDivElement>(null);
|
||||
const renderedHeight = useRef(0);
|
||||
const renderedWidth = useRef(0);
|
||||
const inView = useRef(!!props.defaultShow);
|
||||
const [_, triggerRender] = useState(0);
|
||||
const checkInView = useThrottledCallback(
|
||||
() => {
|
||||
const parent = props.parentRef?.current;
|
||||
const md = mdRef.current;
|
||||
if (parent && md && !props.defaultShow) {
|
||||
const parentBounds = parent.getBoundingClientRect();
|
||||
const twoScreenHeight = Math.max(500, parentBounds.height * 2);
|
||||
const mdBounds = md.getBoundingClientRect();
|
||||
const parentTop = parentBounds.top - twoScreenHeight;
|
||||
const parentBottom = parentBounds.bottom + twoScreenHeight;
|
||||
const isOverlap =
|
||||
Math.max(parentTop, mdBounds.top) <=
|
||||
Math.min(parentBottom, mdBounds.bottom);
|
||||
inView.current = isOverlap;
|
||||
triggerRender(Date.now());
|
||||
}
|
||||
|
||||
const parent = props.parentRef?.current;
|
||||
const md = mdRef.current;
|
||||
if (inView.current && md) {
|
||||
const rect = md.getBoundingClientRect();
|
||||
renderedHeight.current = Math.max(renderedHeight.current, rect.height);
|
||||
renderedWidth.current = Math.max(renderedWidth.current, rect.width);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
},
|
||||
300,
|
||||
{
|
||||
leading: true,
|
||||
trailing: true,
|
||||
},
|
||||
);
|
||||
|
||||
const checkInView = () => {
|
||||
if (parent && md) {
|
||||
const parentBounds = parent.getBoundingClientRect();
|
||||
const twoScreenHeight = Math.max(500, parentBounds.height * 2);
|
||||
const mdBounds = md.getBoundingClientRect();
|
||||
const parentTop = parentBounds.top - twoScreenHeight;
|
||||
const parentBottom = parentBounds.bottom + twoScreenHeight;
|
||||
const isOverlap =
|
||||
Math.max(parentTop, mdBounds.top) <=
|
||||
Math.min(parentBottom, mdBounds.bottom);
|
||||
inView.current = isOverlap;
|
||||
}
|
||||
useEffect(() => {
|
||||
props.parentRef?.current?.addEventListener("scroll", checkInView);
|
||||
checkInView();
|
||||
return () =>
|
||||
props.parentRef?.current?.removeEventListener("scroll", checkInView);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
if (inView.current && md) {
|
||||
renderedHeight.current = Math.max(
|
||||
renderedHeight.current,
|
||||
md.getBoundingClientRect().height,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
setTimeout(() => checkInView(), 1);
|
||||
const getSize = (x: number) => (!inView.current && x > 0 ? x : "auto");
|
||||
|
||||
return (
|
||||
<div
|
||||
className="markdown-body"
|
||||
style={{
|
||||
fontSize: `${props.fontSize ?? 14}px`,
|
||||
height:
|
||||
!inView.current && renderedHeight.current > 0
|
||||
? renderedHeight.current
|
||||
: "auto",
|
||||
height: getSize(renderedHeight.current),
|
||||
width: getSize(renderedWidth.current),
|
||||
}}
|
||||
ref={mdRef}
|
||||
onContextMenu={props.onContextMenu}
|
||||
|
||||
@@ -185,7 +185,12 @@ function ContextPromptItem(props: {
|
||||
className={chatStyle["context-content"]}
|
||||
rows={focusingInput ? 5 : 1}
|
||||
onFocus={() => setFocusingInput(true)}
|
||||
onBlur={() => setFocusingInput(false)}
|
||||
onBlur={() => {
|
||||
setFocusingInput(false);
|
||||
// If the selection is not removed when the user loses focus, some
|
||||
// extensions like "Translate" will always display a floating bar
|
||||
window?.getSelection()?.removeAllRanges();
|
||||
}}
|
||||
onInput={(e) =>
|
||||
props.update({
|
||||
...props.prompt,
|
||||
|
||||
@@ -1,596 +1 @@
|
||||
export {};
|
||||
// import { useState, useEffect, useMemo, HTMLProps, useRef } from "react";
|
||||
|
||||
// import styles from "./settings.module.scss";
|
||||
|
||||
// import ResetIcon from "../icons/reload.svg";
|
||||
// import AddIcon from "../icons/add.svg";
|
||||
// import CloseIcon from "../icons/close.svg";
|
||||
// import CopyIcon from "../icons/copy.svg";
|
||||
// import ClearIcon from "../icons/clear.svg";
|
||||
// import LoadingIcon from "../icons/three-dots.svg";
|
||||
// import EditIcon from "../icons/edit.svg";
|
||||
// import EyeIcon from "../icons/eye.svg";
|
||||
// import {
|
||||
// Input,
|
||||
// List,
|
||||
// ListItem,
|
||||
// Modal,
|
||||
// PasswordInput,
|
||||
// Popover,
|
||||
// Select,
|
||||
// } from "./ui-lib";
|
||||
// import { ModelConfigList } from "./model-config";
|
||||
|
||||
// import { IconButton } from "./button";
|
||||
// import {
|
||||
// SubmitKey,
|
||||
// useChatStore,
|
||||
// Theme,
|
||||
// useUpdateStore,
|
||||
// useAccessStore,
|
||||
// useAppConfig,
|
||||
// } from "../store";
|
||||
|
||||
// import Locale, {
|
||||
// AllLangs,
|
||||
// ALL_LANG_OPTIONS,
|
||||
// changeLang,
|
||||
// getLang,
|
||||
// } from "../locales";
|
||||
// import { copyToClipboard } from "../utils";
|
||||
// import Link from "next/link";
|
||||
// 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";
|
||||
// import { Avatar, AvatarPicker } from "./emoji";
|
||||
|
||||
// function EditPromptModal(props: { id: number; onClose: () => void }) {
|
||||
// const promptStore = usePromptStore();
|
||||
// const prompt = promptStore.get(props.id);
|
||||
|
||||
// return prompt ? (
|
||||
// <div className="modal-mask">
|
||||
// <Modal
|
||||
// title={Locale.Settings.Prompt.EditModal.Title}
|
||||
// onClose={props.onClose}
|
||||
// actions={[
|
||||
// <IconButton
|
||||
// key=""
|
||||
// onClick={props.onClose}
|
||||
// text={Locale.UI.Confirm}
|
||||
// bordered
|
||||
// />,
|
||||
// ]}
|
||||
// >
|
||||
// <div className={styles["edit-prompt-modal"]}>
|
||||
// <input
|
||||
// type="text"
|
||||
// value={prompt.title}
|
||||
// readOnly={!prompt.isUser}
|
||||
// className={styles["edit-prompt-title"]}
|
||||
// onInput={(e) =>
|
||||
// promptStore.update(
|
||||
// props.id,
|
||||
// (prompt) => (prompt.title = e.currentTarget.value),
|
||||
// )
|
||||
// }
|
||||
// ></input>
|
||||
// <Input
|
||||
// value={prompt.content}
|
||||
// readOnly={!prompt.isUser}
|
||||
// className={styles["edit-prompt-content"]}
|
||||
// rows={10}
|
||||
// onInput={(e) =>
|
||||
// promptStore.update(
|
||||
// props.id,
|
||||
// (prompt) => (prompt.content = e.currentTarget.value),
|
||||
// )
|
||||
// }
|
||||
// ></Input>
|
||||
// </div>
|
||||
// </Modal>
|
||||
// </div>
|
||||
// ) : 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<Prompt[]>([]);
|
||||
// const prompts = searchInput.length > 0 ? searchPrompts : allPrompts;
|
||||
|
||||
// const [editingPromptId, setEditingPromptId] = useState<number>();
|
||||
|
||||
// useEffect(() => {
|
||||
// if (searchInput.length > 0) {
|
||||
// const searchResult = SearchService.search(searchInput);
|
||||
// setSearchPrompts(searchResult);
|
||||
// } else {
|
||||
// setSearchPrompts([]);
|
||||
// }
|
||||
// }, [searchInput]);
|
||||
|
||||
// return (
|
||||
// <div className="modal-mask">
|
||||
// <Modal
|
||||
// title={Locale.Settings.Prompt.Modal.Title}
|
||||
// onClose={() => props.onClose?.()}
|
||||
// actions={[
|
||||
// <IconButton
|
||||
// key="add"
|
||||
// onClick={() =>
|
||||
// promptStore.add({
|
||||
// title: "Empty Prompt",
|
||||
// content: "Empty Prompt Content",
|
||||
// })
|
||||
// }
|
||||
// icon={<AddIcon />}
|
||||
// bordered
|
||||
// text={Locale.Settings.Prompt.Modal.Add}
|
||||
// />,
|
||||
// ]}
|
||||
// >
|
||||
// <div className={styles["user-prompt-modal"]}>
|
||||
// <input
|
||||
// type="text"
|
||||
// className={styles["user-prompt-search"]}
|
||||
// placeholder={Locale.Settings.Prompt.Modal.Search}
|
||||
// value={searchInput}
|
||||
// onInput={(e) => setSearchInput(e.currentTarget.value)}
|
||||
// ></input>
|
||||
|
||||
// <div className={styles["user-prompt-list"]}>
|
||||
// {prompts.map((v, _) => (
|
||||
// <div className={styles["user-prompt-item"]} key={v.id ?? v.title}>
|
||||
// <div className={styles["user-prompt-header"]}>
|
||||
// <div className={styles["user-prompt-title"]}>{v.title}</div>
|
||||
// <div className={styles["user-prompt-content"] + " one-line"}>
|
||||
// {v.content}
|
||||
// </div>
|
||||
// </div>
|
||||
|
||||
// <div className={styles["user-prompt-buttons"]}>
|
||||
// {v.isUser && (
|
||||
// <IconButton
|
||||
// icon={<ClearIcon />}
|
||||
// className={styles["user-prompt-button"]}
|
||||
// onClick={() => promptStore.remove(v.id!)}
|
||||
// />
|
||||
// )}
|
||||
// {v.isUser ? (
|
||||
// <IconButton
|
||||
// icon={<EditIcon />}
|
||||
// className={styles["user-prompt-button"]}
|
||||
// onClick={() => setEditingPromptId(v.id)}
|
||||
// />
|
||||
// ) : (
|
||||
// <IconButton
|
||||
// icon={<EyeIcon />}
|
||||
// className={styles["user-prompt-button"]}
|
||||
// onClick={() => setEditingPromptId(v.id)}
|
||||
// />
|
||||
// )}
|
||||
// <IconButton
|
||||
// icon={<CopyIcon />}
|
||||
// className={styles["user-prompt-button"]}
|
||||
// onClick={() => copyToClipboard(v.content)}
|
||||
// />
|
||||
// </div>
|
||||
// </div>
|
||||
// ))}
|
||||
// </div>
|
||||
// </div>
|
||||
// </Modal>
|
||||
|
||||
// {editingPromptId !== undefined && (
|
||||
// <EditPromptModal
|
||||
// id={editingPromptId!}
|
||||
// onClose={() => setEditingPromptId(undefined)}
|
||||
// />
|
||||
// )}
|
||||
// </div>
|
||||
// );
|
||||
// }
|
||||
|
||||
// function formatVersionDate(t: string) {
|
||||
// const d = new Date(+t);
|
||||
// const year = d.getUTCFullYear();
|
||||
// const month = d.getUTCMonth() + 1;
|
||||
// const day = d.getUTCDate();
|
||||
|
||||
// return [
|
||||
// year.toString(),
|
||||
// month.toString().padStart(2, "0"),
|
||||
// day.toString().padStart(2, "0"),
|
||||
// ].join("");
|
||||
// }
|
||||
|
||||
// export function Settings() {
|
||||
// const navigate = useNavigate();
|
||||
// const [showEmojiPicker, setShowEmojiPicker] = useState(false);
|
||||
// const config = useAppConfig();
|
||||
// const updateConfig = config.update;
|
||||
// const resetConfig = config.reset;
|
||||
// const chatStore = useChatStore();
|
||||
|
||||
// const updateStore = useUpdateStore();
|
||||
// const [checkingUpdate, setCheckingUpdate] = useState(false);
|
||||
// const currentVersion = formatVersionDate(updateStore.version);
|
||||
// const remoteId = formatVersionDate(updateStore.remoteVersion);
|
||||
// const hasNewVersion = currentVersion !== remoteId;
|
||||
|
||||
// function checkUpdate(force = false) {
|
||||
// setCheckingUpdate(true);
|
||||
// updateStore.getLatestVersion(force).then(() => {
|
||||
// setCheckingUpdate(false);
|
||||
// });
|
||||
|
||||
// console.log(
|
||||
// "[Update] local version ",
|
||||
// new Date(+updateStore.version).toLocaleString(),
|
||||
// );
|
||||
// console.log(
|
||||
// "[Update] remote version ",
|
||||
// new Date(+updateStore.remoteVersion).toLocaleString(),
|
||||
// );
|
||||
// }
|
||||
|
||||
// const usage = {
|
||||
// used: updateStore.used,
|
||||
// subscription: updateStore.subscription,
|
||||
// };
|
||||
// const [loadingUsage, setLoadingUsage] = useState(false);
|
||||
// function checkUsage(force = false) {
|
||||
// setLoadingUsage(true);
|
||||
// updateStore.updateUsage(force).finally(() => {
|
||||
// setLoadingUsage(false);
|
||||
// });
|
||||
// }
|
||||
|
||||
// const accessStore = useAccessStore();
|
||||
// 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);
|
||||
// }
|
||||
// };
|
||||
// document.addEventListener("keydown", keydownEvent);
|
||||
// return () => {
|
||||
// document.removeEventListener("keydown", keydownEvent);
|
||||
// };
|
||||
// // eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
// }, []);
|
||||
|
||||
// return (
|
||||
// <ErrorBoundary>
|
||||
// <div className="window-header">
|
||||
// <div className="window-header-title">
|
||||
// <div className="window-header-main-title">
|
||||
// {Locale.Settings.Title}
|
||||
// </div>
|
||||
// <div className="window-header-sub-title">
|
||||
// {Locale.Settings.SubTitle}
|
||||
// </div>
|
||||
// </div>
|
||||
// <div className="window-actions">
|
||||
// <div className="window-action-button">
|
||||
// <IconButton
|
||||
// icon={<ClearIcon />}
|
||||
// onClick={() => {
|
||||
// if (confirm(Locale.Settings.Actions.ConfirmClearAll)) {
|
||||
// chatStore.clearAllData();
|
||||
// }
|
||||
// }}
|
||||
// bordered
|
||||
// title={Locale.Settings.Actions.ClearAll}
|
||||
// />
|
||||
// </div>
|
||||
// <div className="window-action-button">
|
||||
// <IconButton
|
||||
// icon={<ResetIcon />}
|
||||
// onClick={() => {
|
||||
// if (confirm(Locale.Settings.Actions.ConfirmResetAll)) {
|
||||
// resetConfig();
|
||||
// }
|
||||
// }}
|
||||
// bordered
|
||||
// title={Locale.Settings.Actions.ResetAll}
|
||||
// />
|
||||
// </div>
|
||||
// <div className="window-action-button">
|
||||
// <IconButton
|
||||
// icon={<CloseIcon />}
|
||||
// onClick={() => navigate(Path.Home)}
|
||||
// bordered
|
||||
// title={Locale.Settings.Actions.Close}
|
||||
// />
|
||||
// </div>
|
||||
// </div>
|
||||
// </div>
|
||||
// <div className={styles["settings"]}>
|
||||
// <List>
|
||||
// <ListItem title={Locale.Settings.Avatar}>
|
||||
// <Popover
|
||||
// onClose={() => setShowEmojiPicker(false)}
|
||||
// content={
|
||||
// <AvatarPicker
|
||||
// onEmojiClick={(avatar: string) => {
|
||||
// updateConfig((config) => (config.avatar = avatar));
|
||||
// setShowEmojiPicker(false);
|
||||
// }}
|
||||
// />
|
||||
// }
|
||||
// open={showEmojiPicker}
|
||||
// >
|
||||
// <div
|
||||
// className={styles.avatar}
|
||||
// onClick={() => setShowEmojiPicker(true)}
|
||||
// >
|
||||
// <Avatar avatar={config.avatar} />
|
||||
// </div>
|
||||
// </Popover>
|
||||
// </ListItem>
|
||||
|
||||
// <ListItem
|
||||
// title={Locale.Settings.Update.Version(currentVersion ?? "unknown")}
|
||||
// subTitle={
|
||||
// checkingUpdate
|
||||
// ? Locale.Settings.Update.IsChecking
|
||||
// : hasNewVersion
|
||||
// ? Locale.Settings.Update.FoundUpdate(remoteId ?? "ERROR")
|
||||
// : Locale.Settings.Update.IsLatest
|
||||
// }
|
||||
// >
|
||||
// {checkingUpdate ? (
|
||||
// <LoadingIcon />
|
||||
// ) : hasNewVersion ? (
|
||||
// <Link href={UPDATE_URL} target="_blank" className="link">
|
||||
// {Locale.Settings.Update.GoToUpdate}
|
||||
// </Link>
|
||||
// ) : (
|
||||
// <IconButton
|
||||
// icon={<ResetIcon></ResetIcon>}
|
||||
// text={Locale.Settings.Update.CheckUpdate}
|
||||
// onClick={() => checkUpdate(true)}
|
||||
// />
|
||||
// )}
|
||||
// </ListItem>
|
||||
|
||||
// <ListItem title={Locale.Settings.SendKey}>
|
||||
// <Select
|
||||
// value={config.submitKey}
|
||||
// onChange={(e) => {
|
||||
// updateConfig(
|
||||
// (config) =>
|
||||
// (config.submitKey = e.target.value as any as SubmitKey),
|
||||
// );
|
||||
// }}
|
||||
// >
|
||||
// {Object.values(SubmitKey).map((v) => (
|
||||
// <option value={v} key={v}>
|
||||
// {v}
|
||||
// </option>
|
||||
// ))}
|
||||
// </Select>
|
||||
// </ListItem>
|
||||
|
||||
// <ListItem title={Locale.Settings.Theme}>
|
||||
// <Select
|
||||
// value={config.theme}
|
||||
// onChange={(e) => {
|
||||
// updateConfig(
|
||||
// (config) => (config.theme = e.target.value as any as Theme),
|
||||
// );
|
||||
// }}
|
||||
// >
|
||||
// {Object.values(Theme).map((v) => (
|
||||
// <option value={v} key={v}>
|
||||
// {v}
|
||||
// </option>
|
||||
// ))}
|
||||
// </Select>
|
||||
// </ListItem>
|
||||
|
||||
// <ListItem title={Locale.Settings.Lang.Name}>
|
||||
// <Select
|
||||
// value={getLang()}
|
||||
// onChange={(e) => {
|
||||
// changeLang(e.target.value as any);
|
||||
// }}
|
||||
// >
|
||||
// {AllLangs.map((lang) => (
|
||||
// <option value={lang} key={lang}>
|
||||
// {ALL_LANG_OPTIONS[lang]}
|
||||
// </option>
|
||||
// ))}
|
||||
// </Select>
|
||||
// </ListItem>
|
||||
|
||||
// <ListItem
|
||||
// title={Locale.Settings.FontSize.Title}
|
||||
// subTitle={Locale.Settings.FontSize.SubTitle}
|
||||
// >
|
||||
// <InputRange
|
||||
// title={`${config.fontSize ?? 14}px`}
|
||||
// value={config.fontSize}
|
||||
// min="12"
|
||||
// max="18"
|
||||
// step="1"
|
||||
// onChange={(e) =>
|
||||
// updateConfig(
|
||||
// (config) =>
|
||||
// (config.fontSize = Number.parseInt(e.currentTarget.value)),
|
||||
// )
|
||||
// }
|
||||
// ></InputRange>
|
||||
// </ListItem>
|
||||
|
||||
// <ListItem
|
||||
// title={Locale.Settings.SendPreviewBubble.Title}
|
||||
// subTitle={Locale.Settings.SendPreviewBubble.SubTitle}
|
||||
// >
|
||||
// <input
|
||||
// type="checkbox"
|
||||
// checked={config.sendPreviewBubble}
|
||||
// onChange={(e) =>
|
||||
// updateConfig(
|
||||
// (config) =>
|
||||
// (config.sendPreviewBubble = e.currentTarget.checked),
|
||||
// )
|
||||
// }
|
||||
// ></input>
|
||||
// </ListItem>
|
||||
|
||||
// <ListItem
|
||||
// title={Locale.Settings.Mask.Title}
|
||||
// subTitle={Locale.Settings.Mask.SubTitle}
|
||||
// >
|
||||
// <input
|
||||
// type="checkbox"
|
||||
// checked={!config.dontShowMaskSplashScreen}
|
||||
// onChange={(e) =>
|
||||
// updateConfig(
|
||||
// (config) =>
|
||||
// (config.dontShowMaskSplashScreen =
|
||||
// !e.currentTarget.checked),
|
||||
// )
|
||||
// }
|
||||
// ></input>
|
||||
// </ListItem>
|
||||
// </List>
|
||||
|
||||
// <List>
|
||||
// {enabledAccessControl ? (
|
||||
// <ListItem
|
||||
// title={Locale.Settings.AccessCode.Title}
|
||||
// subTitle={Locale.Settings.AccessCode.SubTitle}
|
||||
// >
|
||||
// <PasswordInput
|
||||
// value={accessStore.accessCode}
|
||||
// type="text"
|
||||
// placeholder={Locale.Settings.AccessCode.Placeholder}
|
||||
// onChange={(e) => {
|
||||
// accessStore.updateCode(e.currentTarget.value);
|
||||
// }}
|
||||
// />
|
||||
// </ListItem>
|
||||
// ) : (
|
||||
// <></>
|
||||
// )}
|
||||
|
||||
// {!accessStore.hideUserApiKey ? (
|
||||
// <ListItem
|
||||
// title={Locale.Settings.Token.Title}
|
||||
// subTitle={Locale.Settings.Token.SubTitle}
|
||||
// >
|
||||
// <PasswordInput
|
||||
// value={accessStore.token}
|
||||
// type="text"
|
||||
// placeholder={Locale.Settings.Token.Placeholder}
|
||||
// onChange={(e) => {
|
||||
// accessStore.updateToken(e.currentTarget.value);
|
||||
// }}
|
||||
// />
|
||||
// </ListItem>
|
||||
// ) : null}
|
||||
|
||||
// <ListItem
|
||||
// title={Locale.Settings.Usage.Title}
|
||||
// subTitle={
|
||||
// showUsage
|
||||
// ? loadingUsage
|
||||
// ? Locale.Settings.Usage.IsChecking
|
||||
// : Locale.Settings.Usage.SubTitle(
|
||||
// usage?.used ?? "[?]",
|
||||
// usage?.subscription ?? "[?]",
|
||||
// )
|
||||
// : Locale.Settings.Usage.NoAccess
|
||||
// }
|
||||
// >
|
||||
// {!showUsage || loadingUsage ? (
|
||||
// <div />
|
||||
// ) : (
|
||||
// <IconButton
|
||||
// icon={<ResetIcon></ResetIcon>}
|
||||
// text={Locale.Settings.Usage.Check}
|
||||
// onClick={() => checkUsage(true)}
|
||||
// />
|
||||
// )}
|
||||
// </ListItem>
|
||||
// </List>
|
||||
|
||||
// <List>
|
||||
// <ListItem
|
||||
// title={Locale.Settings.Prompt.Disable.Title}
|
||||
// subTitle={Locale.Settings.Prompt.Disable.SubTitle}
|
||||
// >
|
||||
// <input
|
||||
// type="checkbox"
|
||||
// checked={config.disablePromptHint}
|
||||
// onChange={(e) =>
|
||||
// updateConfig(
|
||||
// (config) =>
|
||||
// (config.disablePromptHint = e.currentTarget.checked),
|
||||
// )
|
||||
// }
|
||||
// ></input>
|
||||
// </ListItem>
|
||||
|
||||
// <ListItem
|
||||
// title={Locale.Settings.Prompt.List}
|
||||
// subTitle={Locale.Settings.Prompt.ListCount(
|
||||
// builtinCount,
|
||||
// customCount,
|
||||
// )}
|
||||
// >
|
||||
// <IconButton
|
||||
// icon={<EditIcon />}
|
||||
// text={Locale.Settings.Prompt.Edit}
|
||||
// onClick={() => setShowPromptModal(true)}
|
||||
// />
|
||||
// </ListItem>
|
||||
// </List>
|
||||
|
||||
// <List>
|
||||
// <ModelConfigList
|
||||
// modelConfig={config.modelConfig}
|
||||
// updateConfig={(updater) => {
|
||||
// const modelConfig = { ...config.modelConfig };
|
||||
// updater(modelConfig);
|
||||
// config.update((config) => (config.modelConfig = modelConfig));
|
||||
// }}
|
||||
// />
|
||||
// </List>
|
||||
|
||||
// {shouldShowPromptModal && (
|
||||
// <UserPromptModal onClose={() => setShowPromptModal(false)} />
|
||||
// )}
|
||||
// </div>
|
||||
// </ErrorBoundary>
|
||||
// );
|
||||
// }
|
||||
|
||||
+17
-13
@@ -1,16 +1,3 @@
|
||||
const COMMIT_ID: string = (() => {
|
||||
try {
|
||||
const childProcess = require("child_process");
|
||||
return childProcess
|
||||
.execSync('git log -1 --format="%at000" --date=unix')
|
||||
.toString()
|
||||
.trim();
|
||||
} catch (e) {
|
||||
console.error("[Build Config] No git or not from git repo.");
|
||||
return "unknown";
|
||||
}
|
||||
})();
|
||||
|
||||
export const getBuildConfig = () => {
|
||||
if (typeof process === "undefined") {
|
||||
throw Error(
|
||||
@@ -18,7 +5,24 @@ export const getBuildConfig = () => {
|
||||
);
|
||||
}
|
||||
|
||||
const COMMIT_ID: string = (() => {
|
||||
try {
|
||||
const childProcess = require("child_process");
|
||||
return childProcess
|
||||
.execSync('git log -1 --format="%at000" --date=unix')
|
||||
.toString()
|
||||
.trim();
|
||||
} catch (e) {
|
||||
console.error("[Build Config] No git or not from git repo.");
|
||||
return "unknown";
|
||||
}
|
||||
})();
|
||||
|
||||
return {
|
||||
commitId: COMMIT_ID,
|
||||
buildMode: process.env.BUILD_MODE ?? "standalone",
|
||||
isApp: !!process.env.BUILD_APP,
|
||||
};
|
||||
};
|
||||
|
||||
export type BuildConfig = ReturnType<typeof getBuildConfig>;
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import { BuildConfig, getBuildConfig } from "./build";
|
||||
|
||||
export function getClientConfig() {
|
||||
if (typeof document !== "undefined") {
|
||||
// client side
|
||||
return JSON.parse(queryMeta("config")) as BuildConfig;
|
||||
}
|
||||
|
||||
if (typeof process !== "undefined") {
|
||||
// server side
|
||||
return getBuildConfig();
|
||||
}
|
||||
}
|
||||
|
||||
function queryMeta(key: string, defaultValue?: string): string {
|
||||
let ret: string;
|
||||
if (document) {
|
||||
const meta = document.head.querySelector(
|
||||
`meta[name='${key}']`,
|
||||
) as HTMLMetaElement;
|
||||
ret = meta?.content ?? "";
|
||||
} else {
|
||||
ret = defaultValue ?? "";
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
@@ -10,6 +10,8 @@ declare global {
|
||||
VERCEL?: string;
|
||||
HIDE_USER_API_KEY?: string; // disable user's api key input
|
||||
DISABLE_GPT4?: string; // allow user to use gpt-4 or not
|
||||
BUILD_MODE?: "standalone" | "export";
|
||||
BUILD_APP?: string; // is building desktop app
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
export const DEFAULT_API_HOST = "https://api.openai.com/";
|
||||
|
||||
export enum Path {
|
||||
Home = "/",
|
||||
Chat = "/chat",
|
||||
@@ -36,3 +38,9 @@ export const LAST_INPUT_KEY = "last-input";
|
||||
export const REQUEST_TIMEOUT_MS = 60000;
|
||||
|
||||
export const EXPORT_MESSAGE_CLASS_NAME = "export-markdown";
|
||||
|
||||
export const OpenaiPath = {
|
||||
ChatPath: "v1/chat/completions",
|
||||
UsagePath: "dashboard/billing/usage",
|
||||
SubsPath: "dashboard/billing/subscription",
|
||||
};
|
||||
|
||||
+2
-4
@@ -2,9 +2,7 @@
|
||||
import "./styles/globals.scss";
|
||||
import "./styles/markdown.scss";
|
||||
import "./styles/highlight.scss";
|
||||
import { getBuildConfig } from "./config/build";
|
||||
|
||||
const buildConfig = getBuildConfig();
|
||||
import { getClientConfig } from "./config/client";
|
||||
|
||||
export const metadata = {
|
||||
title: "ChatGPT Pro",
|
||||
@@ -32,7 +30,7 @@ export default function RootLayout({
|
||||
return (
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta name="version" content={buildConfig.commitId} />
|
||||
<meta name="config" content={JSON.stringify(getClientConfig())} />
|
||||
<link rel="manifest" href="/site.webmanifest"></link>
|
||||
<script src="/serviceWorkerRegister.js" defer></script>
|
||||
</head>
|
||||
|
||||
@@ -27,6 +27,19 @@ const cn = {
|
||||
Retry: "重试",
|
||||
Delete: "删除",
|
||||
},
|
||||
InputActions: {
|
||||
Stop: "停止响应",
|
||||
ToBottom: "滚到最新",
|
||||
Theme: {
|
||||
auto: "自动主题",
|
||||
light: "亮色模式",
|
||||
dark: "深色模式",
|
||||
},
|
||||
Prompt: "快捷指令",
|
||||
Masks: "所有面具",
|
||||
Clear: "清除聊天",
|
||||
Settings: "对话设置",
|
||||
},
|
||||
Rename: "重命名对话",
|
||||
Typing: "正在输入…",
|
||||
Input: (submitKey: string) => {
|
||||
@@ -166,6 +179,10 @@ const cn = {
|
||||
SubTitle: "管理员已开启加密访问",
|
||||
Placeholder: "请输入访问密码",
|
||||
},
|
||||
Endpoint: {
|
||||
Title: "接口地址",
|
||||
SubTitle: "除默认地址外,必须包含 http(s)://",
|
||||
},
|
||||
Model: "模型 (model)",
|
||||
Temperature: {
|
||||
Title: "随机性 (temperature)",
|
||||
|
||||
@@ -28,6 +28,19 @@ const en: RequiredLocaleType = {
|
||||
Retry: "Retry",
|
||||
Delete: "Delete",
|
||||
},
|
||||
InputActions: {
|
||||
Stop: "Stop",
|
||||
ToBottom: "To Latest",
|
||||
Theme: {
|
||||
auto: "Auto",
|
||||
light: "Light Theme",
|
||||
dark: "Dark Theme",
|
||||
},
|
||||
Prompt: "Prompts",
|
||||
Masks: "Masks",
|
||||
Clear: "Clear Context",
|
||||
Settings: "Settings",
|
||||
},
|
||||
Rename: "Rename Chat",
|
||||
Typing: "Typing…",
|
||||
Input: (submitKey: string) => {
|
||||
@@ -167,6 +180,10 @@ const en: RequiredLocaleType = {
|
||||
SubTitle: "Access control enabled",
|
||||
Placeholder: "Need Access Code",
|
||||
},
|
||||
Endpoint: {
|
||||
Title: "Endpoint",
|
||||
SubTitle: "Custom endpoint must start with http(s)://",
|
||||
},
|
||||
Model: "Model",
|
||||
Temperature: {
|
||||
Title: "Temperature",
|
||||
|
||||
@@ -69,7 +69,6 @@ function getLanguage() {
|
||||
try {
|
||||
return navigator.language.toLowerCase();
|
||||
} catch {
|
||||
console.log("[Lang] failed to detect user lang.");
|
||||
return DEFAULT_LANG;
|
||||
}
|
||||
}
|
||||
|
||||
+12
-3
@@ -1,9 +1,10 @@
|
||||
import { create } from "zustand";
|
||||
import { persist } from "zustand/middleware";
|
||||
import { StoreKey } from "../constant";
|
||||
import { DEFAULT_API_HOST, StoreKey } from "../constant";
|
||||
import { getHeaders } from "../client/api";
|
||||
import { BOT_HELLO } from "./chat";
|
||||
import { ALL_MODELS } from "./config";
|
||||
import { getClientConfig } from "../config/client";
|
||||
|
||||
export interface AccessControlStore {
|
||||
accessCode: string;
|
||||
@@ -15,6 +16,7 @@ export interface AccessControlStore {
|
||||
|
||||
updateToken: (_: string) => void;
|
||||
updateCode: (_: string) => void;
|
||||
updateOpenAiUrl: (_: string) => void;
|
||||
enabledAccessControl: () => boolean;
|
||||
isAuthorized: () => boolean;
|
||||
fetch: () => void;
|
||||
@@ -22,6 +24,10 @@ export interface AccessControlStore {
|
||||
|
||||
let fetchState = 0; // 0 not fetch, 1 fetching, 2 done
|
||||
|
||||
const DEFAULT_OPENAI_URL =
|
||||
getClientConfig()?.buildMode === "export" ? DEFAULT_API_HOST : "/api/openai/";
|
||||
console.log("[API] default openai url", DEFAULT_OPENAI_URL);
|
||||
|
||||
export const useAccessStore = create<AccessControlStore>()(
|
||||
persist(
|
||||
(set, get) => ({
|
||||
@@ -29,7 +35,7 @@ export const useAccessStore = create<AccessControlStore>()(
|
||||
accessCode: "",
|
||||
needCode: true,
|
||||
hideUserApiKey: false,
|
||||
openaiUrl: "/api/openai/",
|
||||
openaiUrl: DEFAULT_OPENAI_URL,
|
||||
|
||||
enabledAccessControl() {
|
||||
get().fetch();
|
||||
@@ -42,6 +48,9 @@ export const useAccessStore = create<AccessControlStore>()(
|
||||
updateToken(token: string) {
|
||||
set(() => ({ token }));
|
||||
},
|
||||
updateOpenAiUrl(url: string) {
|
||||
set(() => ({ openaiUrl: url }));
|
||||
},
|
||||
isAuthorized() {
|
||||
get().fetch();
|
||||
|
||||
@@ -51,7 +60,7 @@ export const useAccessStore = create<AccessControlStore>()(
|
||||
);
|
||||
},
|
||||
fetch() {
|
||||
if (fetchState > 0) return;
|
||||
if (fetchState > 0 || getClientConfig()?.buildMode === "export") return;
|
||||
fetchState = 1;
|
||||
fetch("/api/config", {
|
||||
method: "post",
|
||||
|
||||
+10
-7
@@ -11,6 +11,7 @@ import { StoreKey } from "../constant";
|
||||
import { api, RequestMessage } from "../client/api";
|
||||
import { ChatControllerPool } from "../client/controller";
|
||||
import { prettyObject } from "../utils/format";
|
||||
import { estimateTokenLength } from "../utils/token";
|
||||
|
||||
export type ChatMessage = RequestMessage & {
|
||||
date: string;
|
||||
@@ -102,7 +103,7 @@ interface ChatStore {
|
||||
}
|
||||
|
||||
function countMessages(msgs: ChatMessage[]) {
|
||||
return msgs.reduce((pre, cur) => pre + cur.content.length, 0);
|
||||
return msgs.reduce((pre, cur) => pre + estimateTokenLength(cur.content), 0);
|
||||
}
|
||||
|
||||
export const useChatStore = create<ChatStore>()(
|
||||
@@ -226,6 +227,7 @@ export const useChatStore = create<ChatStore>()(
|
||||
|
||||
onNewMessage(message) {
|
||||
get().updateCurrentSession((session) => {
|
||||
session.messages = session.messages.concat();
|
||||
session.lastUpdate = Date.now();
|
||||
});
|
||||
get().updateStat(message);
|
||||
@@ -272,8 +274,7 @@ export const useChatStore = create<ChatStore>()(
|
||||
|
||||
// save user's and bot's message
|
||||
get().updateCurrentSession((session) => {
|
||||
session.messages.push(userMessage);
|
||||
session.messages.push(botMessage);
|
||||
session.messages = session.messages.concat([userMessage, botMessage]);
|
||||
});
|
||||
|
||||
// make request
|
||||
@@ -286,7 +287,9 @@ export const useChatStore = create<ChatStore>()(
|
||||
if (message) {
|
||||
botMessage.content = message;
|
||||
}
|
||||
set(() => ({}));
|
||||
get().updateCurrentSession((session) => {
|
||||
session.messages = session.messages.concat();
|
||||
});
|
||||
},
|
||||
onFinish(message) {
|
||||
botMessage.streaming = false;
|
||||
@@ -298,7 +301,6 @@ export const useChatStore = create<ChatStore>()(
|
||||
sessionIndex,
|
||||
botMessage.id ?? messageIndex,
|
||||
);
|
||||
set(() => ({}));
|
||||
},
|
||||
onError(error) {
|
||||
const isAborted = error.message.includes("aborted");
|
||||
@@ -311,8 +313,9 @@ export const useChatStore = create<ChatStore>()(
|
||||
botMessage.streaming = false;
|
||||
userMessage.isError = !isAborted;
|
||||
botMessage.isError = !isAborted;
|
||||
|
||||
set(() => ({}));
|
||||
get().updateCurrentSession((session) => {
|
||||
session.messages = session.messages.concat();
|
||||
});
|
||||
ChatControllerPool.remove(
|
||||
sessionIndex,
|
||||
botMessage.id ?? messageIndex,
|
||||
|
||||
+23
-2
@@ -1,5 +1,6 @@
|
||||
import { create } from "zustand";
|
||||
import { persist } from "zustand/middleware";
|
||||
import { getClientConfig } from "../config/client";
|
||||
import { StoreKey } from "../constant";
|
||||
|
||||
export enum SubmitKey {
|
||||
@@ -22,7 +23,7 @@ export const DEFAULT_CONFIG = {
|
||||
avatar: "1f603",
|
||||
fontSize: 14,
|
||||
theme: Theme.Auto as Theme,
|
||||
tightBorder: false,
|
||||
tightBorder: !!getClientConfig()?.isApp,
|
||||
sendPreviewBubble: true,
|
||||
sidebarWidth: 300,
|
||||
|
||||
@@ -61,6 +62,10 @@ export const ALL_MODELS = [
|
||||
name: "gpt-4-0314",
|
||||
available: ENABLE_GPT4,
|
||||
},
|
||||
{
|
||||
name: "gpt-4-0613",
|
||||
available: ENABLE_GPT4,
|
||||
},
|
||||
{
|
||||
name: "gpt-4-32k",
|
||||
available: ENABLE_GPT4,
|
||||
@@ -69,6 +74,10 @@ export const ALL_MODELS = [
|
||||
name: "gpt-4-32k-0314",
|
||||
available: ENABLE_GPT4,
|
||||
},
|
||||
{
|
||||
name: "gpt-4-32k-0613",
|
||||
available: ENABLE_GPT4,
|
||||
},
|
||||
{
|
||||
name: "gpt-3.5-turbo",
|
||||
available: true,
|
||||
@@ -77,6 +86,18 @@ export const ALL_MODELS = [
|
||||
name: "gpt-3.5-turbo-0301",
|
||||
available: true,
|
||||
},
|
||||
{
|
||||
name: "gpt-3.5-turbo-0613",
|
||||
available: true,
|
||||
},
|
||||
{
|
||||
name: "gpt-3.5-turbo-16k",
|
||||
available: true,
|
||||
},
|
||||
{
|
||||
name: "gpt-3.5-turbo-16k-0613",
|
||||
available: true,
|
||||
},
|
||||
{
|
||||
name: "qwen-v1", // 通义千问
|
||||
available: false,
|
||||
@@ -117,7 +138,7 @@ export function limitNumber(
|
||||
export function limitModel(name: string) {
|
||||
return ALL_MODELS.some((m) => m.name === name && m.available)
|
||||
? name
|
||||
: ALL_MODELS[4].name;
|
||||
: "gpt-3.5-turbo";
|
||||
}
|
||||
|
||||
export const ModalConfigValidator = {
|
||||
|
||||
+2
-16
@@ -3,7 +3,7 @@ export {};
|
||||
// import { persist } from "zustand/middleware";
|
||||
// import { FETCH_COMMIT_URL, StoreKey } from "../constant";
|
||||
// import { api } from "../client/api";
|
||||
// import { showToast } from "../components/ui-lib";
|
||||
// import { getClientConfig } from "../config/client";
|
||||
|
||||
// export interface UpdateStore {
|
||||
// lastUpdate: number;
|
||||
@@ -18,20 +18,6 @@ export {};
|
||||
// updateUsage: (force?: boolean) => Promise<void>;
|
||||
// }
|
||||
|
||||
// function queryMeta(key: string, defaultValue?: string): string {
|
||||
// let ret: string;
|
||||
// if (document) {
|
||||
// const meta = document.head.querySelector(
|
||||
// `meta[name='${key}']`,
|
||||
// ) as HTMLMetaElement;
|
||||
// ret = meta?.content ?? "";
|
||||
// } else {
|
||||
// ret = defaultValue ?? "";
|
||||
// }
|
||||
|
||||
// return ret;
|
||||
// }
|
||||
|
||||
// const ONE_MINUTE = 60 * 1000;
|
||||
|
||||
// export const useUpdateStore = create<UpdateStore>()(
|
||||
@@ -45,7 +31,7 @@ export {};
|
||||
// version: "unknown",
|
||||
|
||||
// async getLatestVersion(force = false) {
|
||||
// set(() => ({ version: queryMeta("version") ?? "unknown" }));
|
||||
// set(() => ({ version: getClientConfig()?.commitId ?? "unknown" }));
|
||||
|
||||
// const overTenMins = Date.now() - get().lastUpdate > 10 * ONE_MINUTE;
|
||||
// if (!force && !overTenMins) return;
|
||||
|
||||
@@ -1116,4 +1116,4 @@
|
||||
|
||||
.markdown-body ::-webkit-calendar-picker-indicator {
|
||||
filter: invert(50%);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
export function estimateTokenLength(input: string): number {
|
||||
let tokenLength = 0;
|
||||
|
||||
for (let i = 0; i < input.length; i++) {
|
||||
const charCode = input.charCodeAt(i);
|
||||
|
||||
if (charCode < 128) {
|
||||
// ASCII character
|
||||
if (charCode <= 122 && charCode >= 65) {
|
||||
// a-Z
|
||||
tokenLength += 0.25;
|
||||
} else {
|
||||
tokenLength += 0.5;
|
||||
}
|
||||
} else {
|
||||
// Unicode character
|
||||
tokenLength += 1.5;
|
||||
}
|
||||
}
|
||||
|
||||
return tokenLength;
|
||||
}
|
||||
Reference in New Issue
Block a user