fix: align canvas loading and composer controls

This commit is contained in:
Your Name 2026-06-10 23:28:10 +08:00
parent f02d18702a
commit 29033b5757
6 changed files with 167 additions and 33 deletions

View File

@ -1,9 +1,9 @@
"use client";
import { Loader2 } from "lucide-react";
import dynamic from "next/dynamic";
import { useParams, useSearchParams } from "next/navigation";
import { useMemo } from "react";
import { createContext, type ReactNode, useContext, useMemo } from "react";
import { CanvasLogoLoadingView } from "@/components/canvas/canvas-logo-loading";
const CanvasEditorLegacyPage = dynamic(() => import("./CanvasEditorLegacyPageClient"), {
loading: () => <CanvasRouteLoader label="正在加载画布..." />,
@ -21,10 +21,16 @@ const LiblibVideoCanvasReplica = dynamic(
export function CanvasPageClientRouter({
initialId,
initialMode = "",
initialSiteLogo = null,
initialSiteLogoDark = null,
initialSiteName = "AI Chatbot",
initialWorkspaceType = "",
}: {
initialId: string;
initialMode?: string;
initialSiteLogo?: string | null;
initialSiteLogoDark?: string | null;
initialSiteName?: string | null;
initialWorkspaceType?: string;
}) {
const routeParams = useParams();
@ -37,23 +43,69 @@ export function CanvasPageClientRouter({
const requestedAgentMode = mode === "agent";
const isVideoMode = !requestedAgentMode && (mode === "video" || requestedWorkspaceType === "video");
const legacyParams = useMemo(() => Promise.resolve({ id }), [id]);
const loadingBrand = useMemo(
() => ({
siteLogo: initialSiteLogo,
siteLogoDark: initialSiteLogoDark,
siteName: initialSiteName,
}),
[initialSiteLogo, initialSiteLogoDark, initialSiteName]
);
if (!id) {
return <CanvasRouteLoader label="正在加载画布..." />;
return (
<CanvasLoadingBrandProvider value={loadingBrand}>
<CanvasRouteLoader label="正在加载画布..." />
</CanvasLoadingBrandProvider>
);
}
if (isVideoMode) {
return <LiblibVideoCanvasReplica canvasId={id} />;
return (
<CanvasLoadingBrandProvider value={loadingBrand}>
<LiblibVideoCanvasReplica canvasId={id} />
</CanvasLoadingBrandProvider>
);
}
return <CanvasEditorLegacyPage params={legacyParams} />;
return (
<CanvasLoadingBrandProvider value={loadingBrand}>
<CanvasEditorLegacyPage params={legacyParams} />
</CanvasLoadingBrandProvider>
);
}
type CanvasLoadingBrand = {
siteLogo?: string | null;
siteLogoDark?: string | null;
siteName?: string | null;
};
const CanvasLoadingBrandContext = createContext<CanvasLoadingBrand>({});
function CanvasLoadingBrandProvider({
children,
value,
}: {
children: ReactNode;
value: CanvasLoadingBrand;
}) {
return (
<CanvasLoadingBrandContext.Provider value={value}>
{children}
</CanvasLoadingBrandContext.Provider>
);
}
function CanvasRouteLoader({ label }: { label: string }) {
const brand = useContext(CanvasLoadingBrandContext);
return (
<div className="flex h-dvh w-full items-center justify-center bg-[#151515] text-white/70">
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
{label}
</div>
<CanvasLogoLoadingView
label={label}
siteLogo={brand.siteLogo}
siteLogoDark={brand.siteLogoDark}
siteName={brand.siteName}
/>
);
}

View File

@ -1,5 +1,6 @@
import { Loader2 } from "lucide-react";
import { Suspense } from "react";
import { CanvasLogoLoadingView } from "@/components/canvas/canvas-logo-loading";
import { getSiteSettings } from "@/lib/db/queries";
import { CanvasPageClientRouter } from "./CanvasPageClientRouter";
type CanvasEditorPageProps = {
@ -20,12 +21,21 @@ export default async function CanvasEditorPage({
}: CanvasEditorPageProps) {
const { id } = await params;
const resolvedSearchParams = searchParams ? await searchParams : {};
const siteSettings = await getSiteSettings().catch(() => null);
const loadingBrand = {
siteLogo: siteSettings?.siteLogo,
siteLogoDark: siteSettings?.siteLogoDark,
siteName: siteSettings?.siteName,
};
return (
<Suspense fallback={<CanvasPageLoader />}>
<Suspense fallback={<CanvasPageLoader {...loadingBrand} />}>
<CanvasPageClientRouter
initialId={id}
initialMode={getFirstParamValue(resolvedSearchParams.mode)}
initialSiteLogo={loadingBrand.siteLogo}
initialSiteLogoDark={loadingBrand.siteLogoDark}
initialSiteName={loadingBrand.siteName}
initialWorkspaceType={getFirstParamValue(
resolvedSearchParams.workspaceType
)}
@ -34,11 +44,21 @@ export default async function CanvasEditorPage({
);
}
function CanvasPageLoader() {
function CanvasPageLoader({
siteLogo,
siteLogoDark,
siteName,
}: {
siteLogo?: string | null;
siteLogoDark?: string | null;
siteName?: string | null;
}) {
return (
<div className="flex h-dvh w-full items-center justify-center bg-[#151515] text-white/70">
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
正在加载画布...
</div>
<CanvasLogoLoadingView
label="正在加载画布..."
siteLogo={siteLogo}
siteLogoDark={siteLogoDark}
siteName={siteName}
/>
);
}

View File

@ -1,12 +1,15 @@
import { Loader2 } from "lucide-react";
import { CanvasLogoLoadingView } from "@/components/canvas/canvas-logo-loading";
import { getSiteSettings } from "@/lib/db/queries";
export default async function CanvasLoading() {
const settings = await getSiteSettings().catch(() => null);
export default function CanvasLoading() {
return (
<div className="flex h-dvh w-screen items-center justify-center bg-[#050607] text-white/70">
<div className="flex flex-col items-center gap-4 rounded-[32px] border border-white/10 bg-white/[0.04] px-8 py-7 shadow-[0_24px_90px_rgba(0,0,0,0.42)]">
<Loader2 className="h-5 w-5 animate-spin text-emerald-300" />
<div className="text-sm tracking-[0.08em]">正在进入工作台...</div>
</div>
</div>
<CanvasLogoLoadingView
label="正在进入工作台"
siteLogo={settings?.siteLogo}
siteLogoDark={settings?.siteLogoDark}
siteName={settings?.siteName}
/>
);
}

View File

@ -10875,7 +10875,7 @@ export function CanvasChatPanel({
? "bg-gradient-to-r from-violet-600 to-fuchsia-600 text-white shadow-md hover:shadow-lg hover:opacity-90 dark:from-violet-500 dark:to-fuchsia-500"
: "bg-muted text-muted-foreground hover:bg-muted/80"
: canSend
? "bg-[#1f2328] text-white shadow-sm hover:bg-[#111418]"
? "bg-[#eef0f3] text-[#20242a] shadow-sm ring-1 ring-inset ring-[#d5dae2] hover:bg-[#e4e7ec] dark:bg-white dark:text-zinc-950 dark:ring-white/15 dark:hover:bg-zinc-100"
: "bg-[#f1f1f2] text-[#b6bac2] hover:bg-[#ececee] dark:bg-[#242529] dark:text-[#777d88] dark:hover:bg-[#2c2d33]"
)}
onClick={handleSend}
@ -10899,7 +10899,7 @@ export function CanvasChatPanel({
canSend &&
(isVideoWorkspace || isDetailGenerationMode
? "opacity-95"
: "opacity-95 invert dark:invert-0")
: "opacity-95")
)}
fallback={Sparkles}
item={getToolbarItem("chatStartGeneration")}
@ -10919,7 +10919,7 @@ export function CanvasChatPanel({
canSend &&
(isVideoWorkspace || isDetailGenerationMode
? "opacity-95"
: "opacity-95 invert dark:invert-0")
: "opacity-95")
)}
fallback={ArrowUp}
item={getToolbarItem(

View File

@ -0,0 +1,59 @@
import { Sparkles } from "lucide-react";
import { cn } from "@/lib/utils";
type CanvasLogoLoadingViewProps = {
label?: string;
siteName?: string | null;
siteLogo?: string | null;
siteLogoDark?: string | null;
};
export function CanvasLogoLoadingView({
label = "正在加载画布...",
siteName = "AI Chatbot",
siteLogo,
siteLogoDark,
}: CanvasLogoLoadingViewProps) {
const resolvedSiteName = String(siteName || "AI Chatbot").trim();
const lightLogo = String(siteLogo || siteLogoDark || "").trim();
const darkLogo = String(siteLogoDark || siteLogo || "").trim();
const hasSeparateDarkLogo = Boolean(darkLogo && darkLogo !== lightLogo);
return (
<div className="flex h-dvh w-screen items-center justify-center bg-[#f4f4f5] text-[#20242a] dark:bg-[#08090b] dark:text-zinc-100">
<div className="flex flex-col items-center gap-5">
<div className="relative flex h-28 w-28 items-center justify-center">
<div className="absolute inset-0 animate-spin rounded-full bg-[conic-gradient(from_0deg,#ff7a00,#ffe04b,#48e6a5,#35a7ff,#b15cff,#ff4aa2,#ff7a00)] opacity-90 blur-[1px]" />
<div className="absolute inset-2 animate-ping rounded-full bg-[conic-gradient(from_180deg,#ff7a00,#35a7ff,#b15cff,#48e6a5,#ff7a00)] opacity-25" />
<div className="absolute inset-[6px] rounded-full bg-[#f4f4f5] dark:bg-[#08090b]" />
<div className="relative flex h-20 w-20 items-center justify-center overflow-hidden rounded-full border border-black/8 bg-white shadow-[0_18px_54px_rgba(15,23,42,0.16)] dark:border-white/10 dark:bg-zinc-950 dark:shadow-[0_24px_70px_rgba(0,0,0,0.45)]">
{lightLogo ? (
<>
<img
alt={resolvedSiteName}
className={cn(
"h-12 w-12 object-contain",
hasSeparateDarkLogo && "dark:hidden"
)}
src={lightLogo}
/>
{hasSeparateDarkLogo ? (
<img
alt={resolvedSiteName}
className="hidden h-12 w-12 object-contain dark:block"
src={darkLogo}
/>
) : null}
</>
) : (
<Sparkles className="h-8 w-8 text-[#20242a] dark:text-white" />
)}
</div>
</div>
<div className="text-[13px] font-medium tracking-[0.18em] text-[#6b7280] dark:text-zinc-400">
{label}
</div>
</div>
</div>
);
}

View File

@ -150,24 +150,24 @@ export function LovartHeroComposer({
</label>
</div>
<div className="flex items-center gap-6 pl-2">
<div className="flex h-11 items-center gap-5 pl-2">
<button
type="button"
className="inline-flex h-9 w-9 items-center justify-center rounded-full text-[#20242a] hover:bg-black/5 dark:text-white/72 dark:hover:bg-white/8"
className="grid h-9 w-9 shrink-0 place-items-center rounded-full p-0 text-[#20242a] leading-none hover:bg-black/5 focus:outline-none focus:ring-0 dark:text-white/72 dark:hover:bg-white/8"
aria-label="开启 Agent 模式"
onClick={onStartAgentMode}
title="开启 Agent 模式"
>
<Lightbulb className="h-5 w-5" />
<Lightbulb className="block h-5 w-5" />
</button>
<Select value={selectedModelId} onValueChange={onModelChange}>
<SelectTrigger
aria-label="选择模型"
className="h-9 w-9 rounded-full border-0 bg-transparent p-0 text-[#20242a] shadow-none hover:bg-black/5 focus:ring-0 focus:ring-offset-0 dark:text-white/72 dark:hover:bg-white/8 [&>svg]:hidden"
className="grid h-9 w-9 shrink-0 place-items-center rounded-full border-0 bg-transparent p-0 text-[#20242a] leading-none shadow-none hover:bg-black/5 focus:outline-none focus:ring-0 focus:ring-offset-0 focus-visible:ring-0 focus-visible:ring-offset-0 dark:text-white/72 dark:hover:bg-white/8 [&>svg]:hidden"
title="选择模型"
>
<span className="inline-flex h-full w-full items-center justify-center">
<Box className="h-5 w-5" />
<span className="grid h-full w-full place-items-center">
<Box className="block h-5 w-5" />
</span>
</SelectTrigger>
<SelectContent