feat: dynamic config

This commit is contained in:
Yidadaa 2023-04-11 02:54:31 +08:00
parent 9b61cb1335
commit d6e6dd09f0
8 changed files with 160 additions and 154 deletions

21
app/api/config/route.ts Normal file
View 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,
});
}

View File

@ -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>
</>
);
}

View File

@ -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);
});
}

View File

@ -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");
}
}

View File

@ -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 />}
</>

View File

@ -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,

View File

@ -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 ?? "";
}
},
}),

View File

@ -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);