forked from XiaoMo/ChatGPT-Next-Web
Merge pull request #1927 from Yidadaa/bugfix-0613
feat: #1000 ready to support client-side only
This commit is contained in:
commit
426ce7fd35
@ -1,5 +1,5 @@
|
|||||||
import { ACCESS_CODE_PREFIX } from "../constant";
|
import { ACCESS_CODE_PREFIX } from "../constant";
|
||||||
import { ChatMessage, ModelConfig, ModelType, useAccessStore } from "../store";
|
import { ChatMessage, ModelType, useAccessStore } from "../store";
|
||||||
import { ChatGPTApi } from "./platforms/openai";
|
import { ChatGPTApi } from "./platforms/openai";
|
||||||
|
|
||||||
export const ROLES = ["system", "user", "assistant"] as const;
|
export const ROLES = ["system", "user", "assistant"] as const;
|
||||||
@ -42,6 +42,27 @@ export abstract class LLMApi {
|
|||||||
abstract usage(): Promise<LLMUsage>;
|
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 {
|
export class ClientApi {
|
||||||
public llm: LLMApi;
|
public llm: LLMApi;
|
||||||
|
|
||||||
|
@ -24,6 +24,7 @@ import {
|
|||||||
import { SideBar } from "./sidebar";
|
import { SideBar } from "./sidebar";
|
||||||
import { useAppConfig } from "../store/config";
|
import { useAppConfig } from "../store/config";
|
||||||
import { AuthPage } from "./auth";
|
import { AuthPage } from "./auth";
|
||||||
|
import { getClientConfig } from "../config/client";
|
||||||
|
|
||||||
export function Loading(props: { noLogo?: boolean }) {
|
export function Loading(props: { noLogo?: boolean }) {
|
||||||
return (
|
return (
|
||||||
@ -147,6 +148,10 @@ function Screen() {
|
|||||||
export function Home() {
|
export function Home() {
|
||||||
useSwitchTheme();
|
useSwitchTheme();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
console.log("[Config] got config from build time", getClientConfig());
|
||||||
|
}, []);
|
||||||
|
|
||||||
if (!useHasHydrated()) {
|
if (!useHasHydrated()) {
|
||||||
return <Loading />;
|
return <Loading />;
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { useState, useEffect, useMemo, HTMLProps, useRef } from "react";
|
import { useState, useEffect, useMemo } from "react";
|
||||||
|
|
||||||
import styles from "./settings.module.scss";
|
import styles from "./settings.module.scss";
|
||||||
|
|
||||||
@ -45,6 +45,7 @@ import { ErrorBoundary } from "./error";
|
|||||||
import { InputRange } from "./input-range";
|
import { InputRange } from "./input-range";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import { Avatar, AvatarPicker } from "./emoji";
|
import { Avatar, AvatarPicker } from "./emoji";
|
||||||
|
import { getClientConfig } from "../config/client";
|
||||||
|
|
||||||
function EditPromptModal(props: { id: number; onClose: () => void }) {
|
function EditPromptModal(props: { id: number; onClose: () => void }) {
|
||||||
const promptStore = usePromptStore();
|
const promptStore = usePromptStore();
|
||||||
@ -541,6 +542,21 @@ export function Settings() {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</ListItem>
|
</ListItem>
|
||||||
|
|
||||||
|
{!accessStore.hideUserApiKey ? (
|
||||||
|
<ListItem
|
||||||
|
title={Locale.Settings.Endpoint.Title}
|
||||||
|
subTitle={Locale.Settings.Endpoint.SubTitle}
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={accessStore.openaiUrl}
|
||||||
|
onChange={(e) =>
|
||||||
|
accessStore.updateOpenAiUrl(e.currentTarget.value)
|
||||||
|
}
|
||||||
|
></input>
|
||||||
|
</ListItem>
|
||||||
|
) : null}
|
||||||
</List>
|
</List>
|
||||||
|
|
||||||
<List>
|
<List>
|
||||||
|
@ -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 = () => {
|
export const getBuildConfig = () => {
|
||||||
if (typeof process === "undefined") {
|
if (typeof process === "undefined") {
|
||||||
throw Error(
|
throw Error(
|
||||||
@ -18,7 +5,23 @@ 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 {
|
return {
|
||||||
commitId: COMMIT_ID,
|
commitId: COMMIT_ID,
|
||||||
|
buildMode: process.env.BUILD_MODE ?? "standalone",
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type BuildConfig = ReturnType<typeof getBuildConfig>;
|
||||||
|
27
app/config/client.ts
Normal file
27
app/config/client.ts
Normal file
@ -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,7 @@ declare global {
|
|||||||
VERCEL?: string;
|
VERCEL?: string;
|
||||||
HIDE_USER_API_KEY?: string; // disable user's api key input
|
HIDE_USER_API_KEY?: string; // disable user's api key input
|
||||||
DISABLE_GPT4?: string; // allow user to use gpt-4 or not
|
DISABLE_GPT4?: string; // allow user to use gpt-4 or not
|
||||||
|
BUILD_MODE?: "standalone" | "export";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -6,6 +6,7 @@ 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_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 FETCH_TAG_URL = `https://api.github.com/repos/${OWNER}/${REPO}/tags?per_page=1`;
|
||||||
export const RUNTIME_CONFIG_DOM = "danger-runtime-config";
|
export const RUNTIME_CONFIG_DOM = "danger-runtime-config";
|
||||||
|
export const DEFAULT_API_HOST = "https://chatgpt.nextweb.fun/api/proxy";
|
||||||
|
|
||||||
export enum Path {
|
export enum Path {
|
||||||
Home = "/",
|
Home = "/",
|
||||||
|
@ -3,8 +3,7 @@ import "./styles/globals.scss";
|
|||||||
import "./styles/markdown.scss";
|
import "./styles/markdown.scss";
|
||||||
import "./styles/highlight.scss";
|
import "./styles/highlight.scss";
|
||||||
import { getBuildConfig } from "./config/build";
|
import { getBuildConfig } from "./config/build";
|
||||||
|
import { getClientConfig } from "./config/client";
|
||||||
const buildConfig = getBuildConfig();
|
|
||||||
|
|
||||||
export const metadata = {
|
export const metadata = {
|
||||||
title: "ChatGPT Next Web",
|
title: "ChatGPT Next Web",
|
||||||
@ -32,7 +31,7 @@ export default function RootLayout({
|
|||||||
return (
|
return (
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta name="version" content={buildConfig.commitId} />
|
<meta name="config" content={JSON.stringify(getClientConfig())} />
|
||||||
<link rel="manifest" href="/site.webmanifest"></link>
|
<link rel="manifest" href="/site.webmanifest"></link>
|
||||||
<script src="/serviceWorkerRegister.js" defer></script>
|
<script src="/serviceWorkerRegister.js" defer></script>
|
||||||
</head>
|
</head>
|
||||||
|
@ -180,6 +180,10 @@ const cn = {
|
|||||||
SubTitle: "管理员已开启加密访问",
|
SubTitle: "管理员已开启加密访问",
|
||||||
Placeholder: "请输入访问密码",
|
Placeholder: "请输入访问密码",
|
||||||
},
|
},
|
||||||
|
Endpoint: {
|
||||||
|
Title: "接口地址",
|
||||||
|
SubTitle: "除默认地址外,必须包含 http(s)://",
|
||||||
|
},
|
||||||
Model: "模型 (model)",
|
Model: "模型 (model)",
|
||||||
Temperature: {
|
Temperature: {
|
||||||
Title: "随机性 (temperature)",
|
Title: "随机性 (temperature)",
|
||||||
|
@ -181,6 +181,10 @@ const en: RequiredLocaleType = {
|
|||||||
SubTitle: "Access control enabled",
|
SubTitle: "Access control enabled",
|
||||||
Placeholder: "Need Access Code",
|
Placeholder: "Need Access Code",
|
||||||
},
|
},
|
||||||
|
Endpoint: {
|
||||||
|
Title: "Endpoint",
|
||||||
|
SubTitle: "Custom endpoint must start with http(s)://",
|
||||||
|
},
|
||||||
Model: "Model",
|
Model: "Model",
|
||||||
Temperature: {
|
Temperature: {
|
||||||
Title: "Temperature",
|
Title: "Temperature",
|
||||||
|
@ -1,9 +1,10 @@
|
|||||||
import { create } from "zustand";
|
import { create } from "zustand";
|
||||||
import { persist } from "zustand/middleware";
|
import { persist } from "zustand/middleware";
|
||||||
import { StoreKey } from "../constant";
|
import { DEFAULT_API_HOST, StoreKey } from "../constant";
|
||||||
import { getHeaders } from "../client/api";
|
import { getHeaders } from "../client/api";
|
||||||
import { BOT_HELLO } from "./chat";
|
import { BOT_HELLO } from "./chat";
|
||||||
import { ALL_MODELS } from "./config";
|
import { ALL_MODELS } from "./config";
|
||||||
|
import { getClientConfig } from "../config/client";
|
||||||
|
|
||||||
export interface AccessControlStore {
|
export interface AccessControlStore {
|
||||||
accessCode: string;
|
accessCode: string;
|
||||||
@ -15,6 +16,7 @@ export interface AccessControlStore {
|
|||||||
|
|
||||||
updateToken: (_: string) => void;
|
updateToken: (_: string) => void;
|
||||||
updateCode: (_: string) => void;
|
updateCode: (_: string) => void;
|
||||||
|
updateOpenAiUrl: (_: string) => void;
|
||||||
enabledAccessControl: () => boolean;
|
enabledAccessControl: () => boolean;
|
||||||
isAuthorized: () => boolean;
|
isAuthorized: () => boolean;
|
||||||
fetch: () => void;
|
fetch: () => void;
|
||||||
@ -22,6 +24,10 @@ export interface AccessControlStore {
|
|||||||
|
|
||||||
let fetchState = 0; // 0 not fetch, 1 fetching, 2 done
|
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>()(
|
export const useAccessStore = create<AccessControlStore>()(
|
||||||
persist(
|
persist(
|
||||||
(set, get) => ({
|
(set, get) => ({
|
||||||
@ -29,7 +35,7 @@ export const useAccessStore = create<AccessControlStore>()(
|
|||||||
accessCode: "",
|
accessCode: "",
|
||||||
needCode: true,
|
needCode: true,
|
||||||
hideUserApiKey: false,
|
hideUserApiKey: false,
|
||||||
openaiUrl: "/api/openai/",
|
openaiUrl: DEFAULT_OPENAI_URL,
|
||||||
|
|
||||||
enabledAccessControl() {
|
enabledAccessControl() {
|
||||||
get().fetch();
|
get().fetch();
|
||||||
@ -42,6 +48,9 @@ export const useAccessStore = create<AccessControlStore>()(
|
|||||||
updateToken(token: string) {
|
updateToken(token: string) {
|
||||||
set(() => ({ token }));
|
set(() => ({ token }));
|
||||||
},
|
},
|
||||||
|
updateOpenAiUrl(url: string) {
|
||||||
|
set(() => ({ openaiUrl: url }));
|
||||||
|
},
|
||||||
isAuthorized() {
|
isAuthorized() {
|
||||||
get().fetch();
|
get().fetch();
|
||||||
|
|
||||||
|
@ -2,7 +2,7 @@ import { create } from "zustand";
|
|||||||
import { persist } from "zustand/middleware";
|
import { persist } from "zustand/middleware";
|
||||||
import { FETCH_COMMIT_URL, StoreKey } from "../constant";
|
import { FETCH_COMMIT_URL, StoreKey } from "../constant";
|
||||||
import { api } from "../client/api";
|
import { api } from "../client/api";
|
||||||
import { showToast } from "../components/ui-lib";
|
import { getClientConfig } from "../config/client";
|
||||||
|
|
||||||
export interface UpdateStore {
|
export interface UpdateStore {
|
||||||
lastUpdate: number;
|
lastUpdate: number;
|
||||||
@ -17,20 +17,6 @@ export interface UpdateStore {
|
|||||||
updateUsage: (force?: boolean) => Promise<void>;
|
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;
|
const ONE_MINUTE = 60 * 1000;
|
||||||
|
|
||||||
export const useUpdateStore = create<UpdateStore>()(
|
export const useUpdateStore = create<UpdateStore>()(
|
||||||
@ -44,7 +30,7 @@ export const useUpdateStore = create<UpdateStore>()(
|
|||||||
version: "unknown",
|
version: "unknown",
|
||||||
|
|
||||||
async getLatestVersion(force = false) {
|
async getLatestVersion(force = false) {
|
||||||
set(() => ({ version: queryMeta("version") ?? "unknown" }));
|
set(() => ({ version: getClientConfig()?.commitId ?? "unknown" }));
|
||||||
|
|
||||||
const overTenMins = Date.now() - get().lastUpdate > 10 * ONE_MINUTE;
|
const overTenMins = Date.now() - get().lastUpdate > 10 * ONE_MINUTE;
|
||||||
if (!force && !overTenMins) return;
|
if (!force && !overTenMins) return;
|
||||||
|
@ -15,6 +15,27 @@ const nextConfig = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (mode !== "export") {
|
if (mode !== "export") {
|
||||||
|
nextConfig.headers = async () => {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
source: "/:path*",
|
||||||
|
headers: [
|
||||||
|
{ key: "Access-Control-Allow-Credentials", value: "true" },
|
||||||
|
{ key: "Access-Control-Allow-Origin", value: "*" },
|
||||||
|
{
|
||||||
|
key: "Access-Control-Allow-Methods",
|
||||||
|
value: "GET,OPTIONS,PATCH,DELETE,POST,PUT",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "Access-Control-Allow-Headers",
|
||||||
|
value:
|
||||||
|
"X-CSRF-Token, X-Requested-With, Accept, Accept-Version, Content-Length, Content-MD5, Content-Type, Date, X-Api-Version",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
nextConfig.rewrites = async () => {
|
nextConfig.rewrites = async () => {
|
||||||
const ret = [
|
const ret = [
|
||||||
{
|
{
|
||||||
|
@ -8,6 +8,7 @@
|
|||||||
"build": "next build",
|
"build": "next build",
|
||||||
"start": "next start",
|
"start": "next start",
|
||||||
"lint": "next lint",
|
"lint": "next lint",
|
||||||
|
"export": "BUILD_MODE=export yarn build",
|
||||||
"prompts": "node ./scripts/fetch-prompts.mjs",
|
"prompts": "node ./scripts/fetch-prompts.mjs",
|
||||||
"prepare": "husky install",
|
"prepare": "husky install",
|
||||||
"proxy-dev": "sh ./scripts/init-proxy.sh && proxychains -f ./scripts/proxychains.conf yarn dev"
|
"proxy-dev": "sh ./scripts/init-proxy.sh && proxychains -f ./scripts/proxychains.conf yarn dev"
|
||||||
|
Loading…
Reference in New Issue
Block a user