mirror of
				https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web.git
				synced 2025-11-04 08:13:43 +08:00 
			
		
		
		
	
		
			
				
	
	
		
			273 lines
		
	
	
		
			7.0 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
			
		
		
	
	
			273 lines
		
	
	
		
			7.0 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
"use client";
 | 
						|
 | 
						|
require("../polyfill");
 | 
						|
 | 
						|
import { useEffect, useState } from "react";
 | 
						|
import styles from "./home.module.scss";
 | 
						|
 | 
						|
import BotIcon from "../icons/bot.svg";
 | 
						|
import LoadingIcon from "../icons/three-dots.svg";
 | 
						|
 | 
						|
import { getCSSVar, useMobileScreen } from "../utils";
 | 
						|
 | 
						|
import dynamic from "next/dynamic";
 | 
						|
import { Path, SlotID } from "../constant";
 | 
						|
import { ErrorBoundary } from "./error";
 | 
						|
 | 
						|
import { getISOLang, getLang } from "../locales";
 | 
						|
 | 
						|
import {
 | 
						|
  HashRouter as Router,
 | 
						|
  Route,
 | 
						|
  Routes,
 | 
						|
  useLocation,
 | 
						|
} from "react-router-dom";
 | 
						|
import { SideBar } from "./sidebar";
 | 
						|
import { useAppConfig } from "../store/config";
 | 
						|
import { AuthPage } from "./auth";
 | 
						|
import { getClientConfig } from "../config/client";
 | 
						|
import { type ClientApi, getClientApi } from "../client/api";
 | 
						|
import { useAccessStore } from "../store";
 | 
						|
import clsx from "clsx";
 | 
						|
import { initializeMcpSystem, isMcpEnabled } from "../mcp/actions";
 | 
						|
 | 
						|
export function Loading(props: { noLogo?: boolean }) {
 | 
						|
  return (
 | 
						|
    <div className={clsx("no-dark", styles["loading-content"])}>
 | 
						|
      {!props.noLogo && <BotIcon />}
 | 
						|
      <LoadingIcon />
 | 
						|
    </div>
 | 
						|
  );
 | 
						|
}
 | 
						|
 | 
						|
const Artifacts = dynamic(async () => (await import("./artifacts")).Artifacts, {
 | 
						|
  loading: () => <Loading noLogo />,
 | 
						|
});
 | 
						|
 | 
						|
const Settings = dynamic(async () => (await import("./settings")).Settings, {
 | 
						|
  loading: () => <Loading noLogo />,
 | 
						|
});
 | 
						|
 | 
						|
const Chat = dynamic(async () => (await import("./chat")).Chat, {
 | 
						|
  loading: () => <Loading noLogo />,
 | 
						|
});
 | 
						|
 | 
						|
const NewChat = dynamic(async () => (await import("./new-chat")).NewChat, {
 | 
						|
  loading: () => <Loading noLogo />,
 | 
						|
});
 | 
						|
 | 
						|
const MaskPage = dynamic(async () => (await import("./mask")).MaskPage, {
 | 
						|
  loading: () => <Loading noLogo />,
 | 
						|
});
 | 
						|
 | 
						|
const PluginPage = dynamic(async () => (await import("./plugin")).PluginPage, {
 | 
						|
  loading: () => <Loading noLogo />,
 | 
						|
});
 | 
						|
 | 
						|
const SearchChat = dynamic(
 | 
						|
  async () => (await import("./search-chat")).SearchChatPage,
 | 
						|
  {
 | 
						|
    loading: () => <Loading noLogo />,
 | 
						|
  },
 | 
						|
);
 | 
						|
 | 
						|
const Sd = dynamic(async () => (await import("./sd")).Sd, {
 | 
						|
  loading: () => <Loading noLogo />,
 | 
						|
});
 | 
						|
 | 
						|
const McpMarketPage = dynamic(
 | 
						|
  async () => (await import("./mcp-market")).McpMarketPage,
 | 
						|
  {
 | 
						|
    loading: () => <Loading noLogo />,
 | 
						|
  },
 | 
						|
);
 | 
						|
 | 
						|
export function useSwitchTheme() {
 | 
						|
  const config = useAppConfig();
 | 
						|
 | 
						|
  useEffect(() => {
 | 
						|
    document.body.classList.remove("light");
 | 
						|
    document.body.classList.remove("dark");
 | 
						|
 | 
						|
    if (config.theme === "dark") {
 | 
						|
      document.body.classList.add("dark");
 | 
						|
    } else if (config.theme === "light") {
 | 
						|
      document.body.classList.add("light");
 | 
						|
    }
 | 
						|
 | 
						|
    const metaDescriptionDark = document.querySelector(
 | 
						|
      'meta[name="theme-color"][media*="dark"]',
 | 
						|
    );
 | 
						|
    const metaDescriptionLight = document.querySelector(
 | 
						|
      'meta[name="theme-color"][media*="light"]',
 | 
						|
    );
 | 
						|
 | 
						|
    if (config.theme === "auto") {
 | 
						|
      metaDescriptionDark?.setAttribute("content", "#151515");
 | 
						|
      metaDescriptionLight?.setAttribute("content", "#fafafa");
 | 
						|
    } else {
 | 
						|
      const themeColor = getCSSVar("--theme-color");
 | 
						|
      metaDescriptionDark?.setAttribute("content", themeColor);
 | 
						|
      metaDescriptionLight?.setAttribute("content", themeColor);
 | 
						|
    }
 | 
						|
  }, [config.theme]);
 | 
						|
}
 | 
						|
 | 
						|
function useHtmlLang() {
 | 
						|
  useEffect(() => {
 | 
						|
    const lang = getISOLang();
 | 
						|
    const htmlLang = document.documentElement.lang;
 | 
						|
 | 
						|
    if (lang !== htmlLang) {
 | 
						|
      document.documentElement.lang = lang;
 | 
						|
    }
 | 
						|
  }, []);
 | 
						|
}
 | 
						|
 | 
						|
const useHasHydrated = () => {
 | 
						|
  const [hasHydrated, setHasHydrated] = useState<boolean>(false);
 | 
						|
 | 
						|
  useEffect(() => {
 | 
						|
    setHasHydrated(true);
 | 
						|
  }, []);
 | 
						|
 | 
						|
  return hasHydrated;
 | 
						|
};
 | 
						|
 | 
						|
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 =
 | 
						|
    googleFontUrl +
 | 
						|
    "/css2?family=" +
 | 
						|
    encodeURIComponent("Noto Sans:wght@300;400;700;900") +
 | 
						|
    "&display=swap";
 | 
						|
  document.head.appendChild(linkEl);
 | 
						|
};
 | 
						|
 | 
						|
export function WindowContent(props: { children: React.ReactNode }) {
 | 
						|
  return (
 | 
						|
    <div className={styles["window-content"]} id={SlotID.AppBody}>
 | 
						|
      {props?.children}
 | 
						|
    </div>
 | 
						|
  );
 | 
						|
}
 | 
						|
 | 
						|
function Screen() {
 | 
						|
  const config = useAppConfig();
 | 
						|
  const location = useLocation();
 | 
						|
  const isArtifact = location.pathname.includes(Path.Artifacts);
 | 
						|
  const isHome = location.pathname === Path.Home;
 | 
						|
  const isAuth = location.pathname === Path.Auth;
 | 
						|
  const isSd = location.pathname === Path.Sd;
 | 
						|
  const isSdNew = location.pathname === Path.SdNew;
 | 
						|
 | 
						|
  const isMobileScreen = useMobileScreen();
 | 
						|
  const shouldTightBorder =
 | 
						|
    getClientConfig()?.isApp || (config.tightBorder && !isMobileScreen);
 | 
						|
 | 
						|
  useEffect(() => {
 | 
						|
    loadAsyncGoogleFont();
 | 
						|
  }, []);
 | 
						|
 | 
						|
  if (isArtifact) {
 | 
						|
    return (
 | 
						|
      <Routes>
 | 
						|
        <Route path="/artifacts/:id" element={<Artifacts />} />
 | 
						|
      </Routes>
 | 
						|
    );
 | 
						|
  }
 | 
						|
  const renderContent = () => {
 | 
						|
    if (isAuth) return <AuthPage />;
 | 
						|
    if (isSd) return <Sd />;
 | 
						|
    if (isSdNew) return <Sd />;
 | 
						|
    return (
 | 
						|
      <>
 | 
						|
        <SideBar
 | 
						|
          className={clsx({
 | 
						|
            [styles["sidebar-show"]]: isHome,
 | 
						|
          })}
 | 
						|
        />
 | 
						|
        <WindowContent>
 | 
						|
          <Routes>
 | 
						|
            <Route path={Path.Home} element={<Chat />} />
 | 
						|
            <Route path={Path.NewChat} element={<NewChat />} />
 | 
						|
            <Route path={Path.Masks} element={<MaskPage />} />
 | 
						|
            <Route path={Path.Plugins} element={<PluginPage />} />
 | 
						|
            <Route path={Path.SearchChat} element={<SearchChat />} />
 | 
						|
            <Route path={Path.Chat} element={<Chat />} />
 | 
						|
            <Route path={Path.Settings} element={<Settings />} />
 | 
						|
            <Route path={Path.McpMarket} element={<McpMarketPage />} />
 | 
						|
          </Routes>
 | 
						|
        </WindowContent>
 | 
						|
      </>
 | 
						|
    );
 | 
						|
  };
 | 
						|
 | 
						|
  return (
 | 
						|
    <div
 | 
						|
      className={clsx(styles.container, {
 | 
						|
        [styles["tight-container"]]: shouldTightBorder,
 | 
						|
        [styles["rtl-screen"]]: getLang() === "ar",
 | 
						|
      })}
 | 
						|
    >
 | 
						|
      {renderContent()}
 | 
						|
    </div>
 | 
						|
  );
 | 
						|
}
 | 
						|
 | 
						|
export function useLoadData() {
 | 
						|
  const config = useAppConfig();
 | 
						|
 | 
						|
  const api: ClientApi = getClientApi(config.modelConfig.providerName);
 | 
						|
 | 
						|
  useEffect(() => {
 | 
						|
    (async () => {
 | 
						|
      const models = await api.llm.models();
 | 
						|
      config.mergeModels(models);
 | 
						|
    })();
 | 
						|
    // eslint-disable-next-line react-hooks/exhaustive-deps
 | 
						|
  }, []);
 | 
						|
}
 | 
						|
 | 
						|
export function Home() {
 | 
						|
  useSwitchTheme();
 | 
						|
  useLoadData();
 | 
						|
  useHtmlLang();
 | 
						|
 | 
						|
  useEffect(() => {
 | 
						|
    console.log("[Config] got config from build time", getClientConfig());
 | 
						|
    useAccessStore.getState().fetch();
 | 
						|
 | 
						|
    const initMcp = async () => {
 | 
						|
      try {
 | 
						|
        const enabled = await isMcpEnabled();
 | 
						|
        if (enabled) {
 | 
						|
          console.log("[MCP] initializing...");
 | 
						|
          await initializeMcpSystem();
 | 
						|
          console.log("[MCP] initialized");
 | 
						|
        }
 | 
						|
      } catch (err) {
 | 
						|
        console.error("[MCP] failed to initialize:", err);
 | 
						|
      }
 | 
						|
    };
 | 
						|
    initMcp();
 | 
						|
  }, []);
 | 
						|
 | 
						|
  if (!useHasHydrated()) {
 | 
						|
    return <Loading />;
 | 
						|
  }
 | 
						|
 | 
						|
  return (
 | 
						|
    <ErrorBoundary>
 | 
						|
      <Router>
 | 
						|
        <Screen />
 | 
						|
      </Router>
 | 
						|
    </ErrorBoundary>
 | 
						|
  );
 | 
						|
}
 |