forked from XiaoMo/ChatGPT-Next-Web
feat: dynamic config
This commit is contained in:
parent
9b61cb1335
commit
d6e6dd09f0
21
app/api/config/route.ts
Normal file
21
app/api/config/route.ts
Normal file
@ -0,0 +1,21 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
|
||||
import { getServerSideConfig } from "../../config/server";
|
||||
|
||||
const serverConfig = getServerSideConfig();
|
||||
|
||||
// Danger! Don not write any secret value here!
|
||||
// 警告!不要在这里写入任何敏感信息!
|
||||
const DANGER_CONFIG = {
|
||||
needCode: serverConfig.needCode,
|
||||
};
|
||||
|
||||
declare global {
|
||||
type DangerConfig = typeof DANGER_CONFIG;
|
||||
}
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
return NextResponse.json({
|
||||
needCode: serverConfig.needCode,
|
||||
});
|
||||
}
|
@ -2,13 +2,7 @@
|
||||
|
||||
require("../polyfill");
|
||||
|
||||
import {
|
||||
useState,
|
||||
useEffect,
|
||||
useRef,
|
||||
useCallback,
|
||||
MouseEventHandler,
|
||||
} from "react";
|
||||
import { useState, useEffect, useRef } from "react";
|
||||
|
||||
import { IconButton } from "./button";
|
||||
import styles from "./home.module.scss";
|
||||
@ -30,7 +24,6 @@ import { Chat } from "./chat";
|
||||
import dynamic from "next/dynamic";
|
||||
import { REPO_URL } from "../constant";
|
||||
import { ErrorBoundary } from "./error";
|
||||
import { useDebounce } from "use-debounce";
|
||||
|
||||
export function Loading(props: { noLogo?: boolean }) {
|
||||
return (
|
||||
@ -165,96 +158,100 @@ function _Home() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`${
|
||||
config.tightBorder && !isMobileScreen()
|
||||
? styles["tight-container"]
|
||||
: styles.container
|
||||
}`}
|
||||
>
|
||||
<>
|
||||
<div
|
||||
className={styles.sidebar + ` ${showSideBar && styles["sidebar-show"]}`}
|
||||
className={`${
|
||||
config.tightBorder && !isMobileScreen()
|
||||
? styles["tight-container"]
|
||||
: styles.container
|
||||
}`}
|
||||
>
|
||||
<div className={styles["sidebar-header"]}>
|
||||
<div className={styles["sidebar-title"]}>ChatGPT Next</div>
|
||||
<div className={styles["sidebar-sub-title"]}>
|
||||
Build your own AI assistant.
|
||||
</div>
|
||||
<div className={styles["sidebar-logo"]}>
|
||||
<ChatGptIcon />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={styles["sidebar-body"]}
|
||||
onClick={() => {
|
||||
setOpenSettings(false);
|
||||
setShowSideBar(false);
|
||||
}}
|
||||
className={
|
||||
styles.sidebar + ` ${showSideBar && styles["sidebar-show"]}`
|
||||
}
|
||||
>
|
||||
<ChatList />
|
||||
</div>
|
||||
|
||||
<div className={styles["sidebar-tail"]}>
|
||||
<div className={styles["sidebar-actions"]}>
|
||||
<div className={styles["sidebar-action"] + " " + styles.mobile}>
|
||||
<IconButton
|
||||
icon={<CloseIcon />}
|
||||
onClick={chatStore.deleteSession}
|
||||
/>
|
||||
<div className={styles["sidebar-header"]}>
|
||||
<div className={styles["sidebar-title"]}>ChatGPT Next</div>
|
||||
<div className={styles["sidebar-sub-title"]}>
|
||||
Build your own AI assistant.
|
||||
</div>
|
||||
<div className={styles["sidebar-action"]}>
|
||||
<div className={styles["sidebar-logo"]}>
|
||||
<ChatGptIcon />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={styles["sidebar-body"]}
|
||||
onClick={() => {
|
||||
setOpenSettings(false);
|
||||
setShowSideBar(false);
|
||||
}}
|
||||
>
|
||||
<ChatList />
|
||||
</div>
|
||||
|
||||
<div className={styles["sidebar-tail"]}>
|
||||
<div className={styles["sidebar-actions"]}>
|
||||
<div className={styles["sidebar-action"] + " " + styles.mobile}>
|
||||
<IconButton
|
||||
icon={<CloseIcon />}
|
||||
onClick={chatStore.deleteSession}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles["sidebar-action"]}>
|
||||
<IconButton
|
||||
icon={<SettingsIcon />}
|
||||
onClick={() => {
|
||||
setOpenSettings(true);
|
||||
setShowSideBar(false);
|
||||
}}
|
||||
shadow
|
||||
/>
|
||||
</div>
|
||||
<div className={styles["sidebar-action"]}>
|
||||
<a href={REPO_URL} target="_blank">
|
||||
<IconButton icon={<GithubIcon />} shadow />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<IconButton
|
||||
icon={<SettingsIcon />}
|
||||
icon={<AddIcon />}
|
||||
text={Locale.Home.NewChat}
|
||||
onClick={() => {
|
||||
setOpenSettings(true);
|
||||
createNewSession();
|
||||
setShowSideBar(false);
|
||||
}}
|
||||
shadow
|
||||
/>
|
||||
</div>
|
||||
<div className={styles["sidebar-action"]}>
|
||||
<a href={REPO_URL} target="_blank">
|
||||
<IconButton icon={<GithubIcon />} shadow />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<IconButton
|
||||
icon={<AddIcon />}
|
||||
text={Locale.Home.NewChat}
|
||||
onClick={() => {
|
||||
createNewSession();
|
||||
setShowSideBar(false);
|
||||
}}
|
||||
shadow
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={styles["sidebar-drag"]}
|
||||
onMouseDown={(e) => onDragMouseDown(e as any)}
|
||||
></div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={styles["sidebar-drag"]}
|
||||
onMouseDown={(e) => onDragMouseDown(e as any)}
|
||||
></div>
|
||||
<div className={styles["window-content"]}>
|
||||
{openSettings ? (
|
||||
<Settings
|
||||
closeSettings={() => {
|
||||
setOpenSettings(false);
|
||||
setShowSideBar(true);
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<Chat
|
||||
key="chat"
|
||||
showSideBar={() => setShowSideBar(true)}
|
||||
sideBarShowing={showSideBar}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={styles["window-content"]}>
|
||||
{openSettings ? (
|
||||
<Settings
|
||||
closeSettings={() => {
|
||||
setOpenSettings(false);
|
||||
setShowSideBar(true);
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<Chat
|
||||
key="chat"
|
||||
showSideBar={() => setShowSideBar(true)}
|
||||
sideBarShowing={showSideBar}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -33,7 +33,6 @@ import { SearchService, usePromptStore } from "../store/prompt";
|
||||
import { requestUsage } from "../requests";
|
||||
import { ErrorBoundary } from "./error";
|
||||
import { InputRange } from "./input-range";
|
||||
import { getClientSideConfig } from "../config/client";
|
||||
|
||||
function SettingItem(props: {
|
||||
title: string;
|
||||
@ -89,13 +88,13 @@ export function Settings(props: { closeSettings: () => void }) {
|
||||
|
||||
const updateStore = useUpdateStore();
|
||||
const [checkingUpdate, setCheckingUpdate] = useState(false);
|
||||
const currentVersion = getClientSideConfig()?.version;
|
||||
const remoteId = updateStore.remoteId;
|
||||
const currentVersion = updateStore.version;
|
||||
const remoteId = updateStore.remoteVersion;
|
||||
const hasNewVersion = currentVersion !== remoteId;
|
||||
|
||||
function checkUpdate(force = false) {
|
||||
setCheckingUpdate(true);
|
||||
updateStore.getLatestCommitId(force).then(() => {
|
||||
updateStore.getLatestVersion(force).then(() => {
|
||||
setCheckingUpdate(false);
|
||||
});
|
||||
}
|
||||
|
@ -1,42 +0,0 @@
|
||||
import { RUNTIME_CONFIG_DOM } from "../constant";
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
export function getClientSideConfig() {
|
||||
if (typeof window === "undefined") {
|
||||
throw Error(
|
||||
"[Client Config] you are importing a browser-only module outside of browser",
|
||||
);
|
||||
}
|
||||
|
||||
const dom = document.getElementById(RUNTIME_CONFIG_DOM);
|
||||
|
||||
if (!dom) {
|
||||
throw Error("[Config] Dont get config before page loading!");
|
||||
}
|
||||
|
||||
try {
|
||||
const fromServerConfig = JSON.parse(dom.innerText) as DangerConfig;
|
||||
const fromBuildConfig = {
|
||||
version: queryMeta("version"),
|
||||
};
|
||||
return {
|
||||
...fromServerConfig,
|
||||
...fromBuildConfig,
|
||||
};
|
||||
} catch (e) {
|
||||
console.error("[Config] failed to parse client config");
|
||||
}
|
||||
}
|
17
app/page.tsx
17
app/page.tsx
@ -1,27 +1,14 @@
|
||||
import { Analytics } from "@vercel/analytics/react";
|
||||
|
||||
import { Home } from "./components/home";
|
||||
|
||||
import { getServerSideConfig } from "./config/server";
|
||||
import { RUNTIME_CONFIG_DOM } from "./constant";
|
||||
|
||||
const serverConfig = getServerSideConfig();
|
||||
|
||||
// Danger! Don not write any secret value here!
|
||||
// 警告!不要在这里写入任何敏感信息!
|
||||
const DANGER_CONFIG = {
|
||||
needCode: serverConfig?.needCode,
|
||||
};
|
||||
|
||||
declare global {
|
||||
type DangerConfig = typeof DANGER_CONFIG;
|
||||
}
|
||||
|
||||
export default function App() {
|
||||
export default async function App() {
|
||||
return (
|
||||
<>
|
||||
<div style={{ display: "none" }} id={RUNTIME_CONFIG_DOM}>
|
||||
{JSON.stringify(DANGER_CONFIG)}
|
||||
</div>
|
||||
<Home />
|
||||
{serverConfig?.isVercel && <Analytics />}
|
||||
</>
|
||||
|
@ -1,26 +1,33 @@
|
||||
import { create } from "zustand";
|
||||
import { persist } from "zustand/middleware";
|
||||
import { getClientSideConfig } from "../config/client";
|
||||
|
||||
export interface AccessControlStore {
|
||||
accessCode: string;
|
||||
token: string;
|
||||
|
||||
needCode: boolean;
|
||||
|
||||
updateToken: (_: string) => void;
|
||||
updateCode: (_: string) => void;
|
||||
enabledAccessControl: () => boolean;
|
||||
isAuthorized: () => boolean;
|
||||
fetch: () => void;
|
||||
}
|
||||
|
||||
export const ACCESS_KEY = "access-control";
|
||||
|
||||
let fetchState = 0; // 0 not fetch, 1 fetching, 2 done
|
||||
|
||||
export const useAccessStore = create<AccessControlStore>()(
|
||||
persist(
|
||||
(set, get) => ({
|
||||
token: "",
|
||||
accessCode: "",
|
||||
needCode: true,
|
||||
enabledAccessControl() {
|
||||
return !!getClientSideConfig()?.needCode;
|
||||
get().fetch();
|
||||
|
||||
return get().needCode;
|
||||
},
|
||||
updateCode(code: string) {
|
||||
set((state) => ({ accessCode: code }));
|
||||
@ -34,6 +41,25 @@ export const useAccessStore = create<AccessControlStore>()(
|
||||
!!get().token || !!get().accessCode || !get().enabledAccessControl()
|
||||
);
|
||||
},
|
||||
fetch() {
|
||||
if (fetchState > 0) return;
|
||||
fetchState = 1;
|
||||
fetch("/api/config", {
|
||||
method: "post",
|
||||
body: null,
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.then((res: DangerConfig) => {
|
||||
console.log("[Config] got config from server", res);
|
||||
set(() => ({ ...res }));
|
||||
})
|
||||
.catch(() => {
|
||||
console.error("[Config] failed to fetch config");
|
||||
})
|
||||
.finally(() => {
|
||||
fetchState = 2;
|
||||
});
|
||||
},
|
||||
}),
|
||||
{
|
||||
name: ACCESS_KEY,
|
||||
|
@ -1,28 +1,46 @@
|
||||
import { create } from "zustand";
|
||||
import { persist } from "zustand/middleware";
|
||||
import { getClientSideConfig } from "../config/client";
|
||||
import { FETCH_COMMIT_URL, FETCH_TAG_URL } from "../constant";
|
||||
|
||||
export interface UpdateStore {
|
||||
lastUpdate: number;
|
||||
remoteId: string;
|
||||
remoteVersion: string;
|
||||
|
||||
getLatestCommitId: (force: boolean) => Promise<string>;
|
||||
version: string;
|
||||
getLatestVersion: (force: boolean) => Promise<string>;
|
||||
}
|
||||
|
||||
export const UPDATE_KEY = "chat-update";
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
export const useUpdateStore = create<UpdateStore>()(
|
||||
persist(
|
||||
(set, get) => ({
|
||||
lastUpdate: 0,
|
||||
remoteId: "",
|
||||
remoteVersion: "",
|
||||
|
||||
version: "unknown",
|
||||
|
||||
async getLatestVersion(force = false) {
|
||||
set(() => ({ version: queryMeta("version") }));
|
||||
|
||||
async getLatestCommitId(force = false) {
|
||||
const overTenMins = Date.now() - get().lastUpdate > 10 * 60 * 1000;
|
||||
const shouldFetch = force || overTenMins;
|
||||
if (!shouldFetch) {
|
||||
return getClientSideConfig()?.version ?? "";
|
||||
return get().version ?? "unknown";
|
||||
}
|
||||
|
||||
try {
|
||||
@ -32,13 +50,13 @@ export const useUpdateStore = create<UpdateStore>()(
|
||||
const remoteId = (data[0].sha as string).substring(0, 7);
|
||||
set(() => ({
|
||||
lastUpdate: Date.now(),
|
||||
remoteId,
|
||||
remoteVersion: remoteId,
|
||||
}));
|
||||
console.log("[Got Upstream] ", remoteId);
|
||||
return remoteId;
|
||||
} catch (error) {
|
||||
console.error("[Fetch Upstream Commit Id]", error);
|
||||
return getClientSideConfig()?.version ?? "";
|
||||
return get().version ?? "";
|
||||
}
|
||||
},
|
||||
}),
|
||||
|
@ -32,7 +32,7 @@ export function middleware(req: NextRequest) {
|
||||
|
||||
// inject api key
|
||||
if (!token) {
|
||||
const apiKey = process.env.OPENAI_API_KEY;
|
||||
const apiKey = serverConfig.apiKey;
|
||||
if (apiKey) {
|
||||
console.log("[Auth] set system token");
|
||||
req.headers.set("token", apiKey);
|
||||
|
Loading…
Reference in New Issue
Block a user