ai-chatbot/app/layout.tsx

224 lines
7.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import type { Metadata } from "next";
import { Toaster } from "sonner";
import { ThemeProvider } from "@/components/theme-provider";
import { FaviconUpdater } from "@/components/favicon-updater";
import { ImageDeliveryOptimizer } from "@/components/image-delivery-optimizer";
import { getSiteSettings } from "@/lib/db/queries";
import "./globals.css";
import { SessionProvider } from "next-auth/react";
const metadataBase = resolveMetadataBase();
export async function generateMetadata(): Promise<Metadata> {
const settings = await getSiteSettings().catch(() => null);
const siteName = String(settings?.siteName || "").trim() || "AI Chatbot";
return {
metadataBase,
title: {
template: `%s | ${siteName}`,
default: siteName,
},
description:
"AI design workspace for Canvas, Agent workflows, image editing, 3D/CAD, and local AI operations.",
icons: {
icon: [
{
url: "/api/favicon",
sizes: "any",
},
],
apple: "/api/favicon",
shortcut: "/api/favicon",
},
};
}
function resolveMetadataBase() {
const candidate =
process.env.NEXT_PUBLIC_APP_URL || process.env.AUTH_URL || process.env.NEXTAUTH_URL;
if (candidate) {
try {
return new URL(candidate);
} catch {
// Keep metadata generation resilient when local env files contain placeholders.
}
}
return new URL("http://localhost:3000");
}
export const viewport = {
maximumScale: 1, // Disable auto-zoom on mobile Safari
};
const LIGHT_THEME_COLOR = "hsl(0 0% 100%)";
const DARK_THEME_COLOR = "hsl(240deg 10% 3.92%)";
const THEME_COLOR_SCRIPT = `\
(function() {
var html = document.documentElement;
var meta = document.querySelector('meta[name="theme-color"]');
if (!meta) {
meta = document.createElement('meta');
meta.setAttribute('name', 'theme-color');
document.head.appendChild(meta);
}
function updateThemeColor() {
var isDark = html.classList.contains('dark');
meta.setAttribute('content', isDark ? '${DARK_THEME_COLOR}' : '${LIGHT_THEME_COLOR}');
}
var observer = new MutationObserver(updateThemeColor);
observer.observe(html, { attributes: true, attributeFilter: ['class'] });
updateThemeColor();
})();`;
const CHUNK_ERROR_RECOVERY_SCRIPT = `\
(function() {
// 安全地访问sessionStorage(某些浏览器设置可能禁止访问)
function safeGetSessionStorage(key) {
try {
return sessionStorage.getItem(key);
} catch (e) {
return null;
}
}
function safeSetSessionStorage(key, value) {
try {
sessionStorage.setItem(key, value);
return true;
} catch (e) {
return false;
}
}
// Keep one attempt per route for the browser session. A cooldown alone can
// turn a recurring editor exception into an endless reload cycle.
var RECOVERY_KEY = 'chunk-reload-attempted-v3:' + window.location.pathname;
var lastReloadAt = Number(safeGetSessionStorage(RECOVERY_KEY) || '0') || 0;
var recoveryNoticeShown = false;
function isFlowBoardEditing() {
var path = window.location.pathname;
return path === '/flow-board' || path.indexOf('/flow-board/') === 0;
}
function showManualRecoveryNotice() {
if (recoveryNoticeShown) return;
recoveryNoticeShown = true;
function show() {
if (!document.body || !isFlowBoardEditing()) return;
var notice = document.createElement('div');
notice.setAttribute('role', 'alert');
notice.setAttribute('data-runtime-recovery-notice', 'true');
notice.style.cssText = 'position:fixed;bottom:24px;left:50%;transform:translateX(-50%);z-index:2147483647;max-width:calc(100vw - 32px);padding:12px 16px;border:1px solid var(--border,#e4e4e7);border-radius:12px;background:var(--background,#fff);color:var(--foreground,#27272a);box-shadow:0 4px 20px #0002;font:14px/1.5 system-ui;';
notice.textContent = '部分页面资源加载失败,请先保存白板后手动刷新页面。';
document.body.appendChild(notice);
}
if (document.body) show();
else window.addEventListener('DOMContentLoaded', show, { once: true });
}
function shouldAutoRecover(message) {
if (!message) return false;
var normalized = String(message).toLowerCase();
return (
normalized.indexOf('chunkloaderror') !== -1 ||
normalized.indexOf('failed to load chunk') !== -1 ||
normalized.indexOf('loading chunk') !== -1 ||
normalized.indexOf('failed to fetch dynamically imported module') !== -1 ||
normalized.indexOf('error loading dynamically imported module') !== -1 ||
normalized.indexOf('importing a module script failed') !== -1 ||
normalized.indexOf('module script load failed') !== -1 ||
normalized.indexOf('failed to find server action') !== -1
);
}
function triggerAutoRecover(reason) {
if (isFlowBoardEditing()) {
showManualRecoveryNotice();
return;
}
if (lastReloadAt) return;
var now = Date.now();
// If storage is blocked, a reload cannot remember its attempt and must not run.
if (!safeSetSessionStorage(RECOVERY_KEY, String(now))) return;
lastReloadAt = now;
console.warn('[RuntimeRecover] 检测到页面资源加载失败,尝试恢复一次...', reason || '');
setTimeout(function() {
if (isFlowBoardEditing()) {
showManualRecoveryNotice();
return;
}
window.location.reload();
}, 800);
}
window.addEventListener('error', function(event) {
var message = '';
if (event && event.message) message += event.message;
if (event && event.error && event.error.message) {
message += ' ' + event.error.message;
}
if (shouldAutoRecover(message)) {
triggerAutoRecover(message);
}
}, true);
window.addEventListener('unhandledrejection', function(event) {
var reason = event && event.reason;
var reasonText = '';
if (typeof reason === 'string') reasonText = reason;
else if (reason && typeof reason.message === 'string') reasonText = reason.message;
if (shouldAutoRecover(reasonText)) {
triggerAutoRecover(reasonText);
}
});
})();`;
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html
// `next-themes` injects an extra classname to the body element to avoid
// visual flicker before hydration. Hence the `suppressHydrationWarning`
// prop is necessary to avoid the React hydration mismatch warning.
// https://github.com/pacocoursey/next-themes?tab=readme-ov-file#with-app
lang="zh-CN"
suppressHydrationWarning
>
<head>
<script
dangerouslySetInnerHTML={{
__html: THEME_COLOR_SCRIPT,
}}
/>
<script
dangerouslySetInnerHTML={{
__html: CHUNK_ERROR_RECOVERY_SCRIPT,
}}
/>
</head>
<body className="antialiased">
<FaviconUpdater />
<ImageDeliveryOptimizer />
<ThemeProvider
attribute="class"
defaultTheme="system"
disableTransitionOnChange
enableSystem
>
<Toaster position="top-center" />
<SessionProvider>{children}</SessionProvider>
</ThemeProvider>
</body>
</html>
);
}