fix: align canvas loading and composer controls
This commit is contained in:
parent
f02d18702a
commit
29033b5757
@ -1,9 +1,9 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { Loader2 } from "lucide-react";
|
|
||||||
import dynamic from "next/dynamic";
|
import dynamic from "next/dynamic";
|
||||||
import { useParams, useSearchParams } from "next/navigation";
|
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"), {
|
const CanvasEditorLegacyPage = dynamic(() => import("./CanvasEditorLegacyPageClient"), {
|
||||||
loading: () => <CanvasRouteLoader label="正在加载画布..." />,
|
loading: () => <CanvasRouteLoader label="正在加载画布..." />,
|
||||||
@ -21,10 +21,16 @@ const LiblibVideoCanvasReplica = dynamic(
|
|||||||
export function CanvasPageClientRouter({
|
export function CanvasPageClientRouter({
|
||||||
initialId,
|
initialId,
|
||||||
initialMode = "",
|
initialMode = "",
|
||||||
|
initialSiteLogo = null,
|
||||||
|
initialSiteLogoDark = null,
|
||||||
|
initialSiteName = "AI Chatbot",
|
||||||
initialWorkspaceType = "",
|
initialWorkspaceType = "",
|
||||||
}: {
|
}: {
|
||||||
initialId: string;
|
initialId: string;
|
||||||
initialMode?: string;
|
initialMode?: string;
|
||||||
|
initialSiteLogo?: string | null;
|
||||||
|
initialSiteLogoDark?: string | null;
|
||||||
|
initialSiteName?: string | null;
|
||||||
initialWorkspaceType?: string;
|
initialWorkspaceType?: string;
|
||||||
}) {
|
}) {
|
||||||
const routeParams = useParams();
|
const routeParams = useParams();
|
||||||
@ -37,23 +43,69 @@ export function CanvasPageClientRouter({
|
|||||||
const requestedAgentMode = mode === "agent";
|
const requestedAgentMode = mode === "agent";
|
||||||
const isVideoMode = !requestedAgentMode && (mode === "video" || requestedWorkspaceType === "video");
|
const isVideoMode = !requestedAgentMode && (mode === "video" || requestedWorkspaceType === "video");
|
||||||
const legacyParams = useMemo(() => Promise.resolve({ id }), [id]);
|
const legacyParams = useMemo(() => Promise.resolve({ id }), [id]);
|
||||||
|
const loadingBrand = useMemo(
|
||||||
|
() => ({
|
||||||
|
siteLogo: initialSiteLogo,
|
||||||
|
siteLogoDark: initialSiteLogoDark,
|
||||||
|
siteName: initialSiteName,
|
||||||
|
}),
|
||||||
|
[initialSiteLogo, initialSiteLogoDark, initialSiteName]
|
||||||
|
);
|
||||||
|
|
||||||
if (!id) {
|
if (!id) {
|
||||||
return <CanvasRouteLoader label="正在加载画布..." />;
|
return (
|
||||||
|
<CanvasLoadingBrandProvider value={loadingBrand}>
|
||||||
|
<CanvasRouteLoader label="正在加载画布..." />
|
||||||
|
</CanvasLoadingBrandProvider>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isVideoMode) {
|
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 }) {
|
function CanvasRouteLoader({ label }: { label: string }) {
|
||||||
|
const brand = useContext(CanvasLoadingBrandContext);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-dvh w-full items-center justify-center bg-[#151515] text-white/70">
|
<CanvasLogoLoadingView
|
||||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
label={label}
|
||||||
{label}
|
siteLogo={brand.siteLogo}
|
||||||
</div>
|
siteLogoDark={brand.siteLogoDark}
|
||||||
|
siteName={brand.siteName}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import { Loader2 } from "lucide-react";
|
|
||||||
import { Suspense } from "react";
|
import { Suspense } from "react";
|
||||||
|
import { CanvasLogoLoadingView } from "@/components/canvas/canvas-logo-loading";
|
||||||
|
import { getSiteSettings } from "@/lib/db/queries";
|
||||||
import { CanvasPageClientRouter } from "./CanvasPageClientRouter";
|
import { CanvasPageClientRouter } from "./CanvasPageClientRouter";
|
||||||
|
|
||||||
type CanvasEditorPageProps = {
|
type CanvasEditorPageProps = {
|
||||||
@ -20,12 +21,21 @@ export default async function CanvasEditorPage({
|
|||||||
}: CanvasEditorPageProps) {
|
}: CanvasEditorPageProps) {
|
||||||
const { id } = await params;
|
const { id } = await params;
|
||||||
const resolvedSearchParams = searchParams ? await searchParams : {};
|
const resolvedSearchParams = searchParams ? await searchParams : {};
|
||||||
|
const siteSettings = await getSiteSettings().catch(() => null);
|
||||||
|
const loadingBrand = {
|
||||||
|
siteLogo: siteSettings?.siteLogo,
|
||||||
|
siteLogoDark: siteSettings?.siteLogoDark,
|
||||||
|
siteName: siteSettings?.siteName,
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Suspense fallback={<CanvasPageLoader />}>
|
<Suspense fallback={<CanvasPageLoader {...loadingBrand} />}>
|
||||||
<CanvasPageClientRouter
|
<CanvasPageClientRouter
|
||||||
initialId={id}
|
initialId={id}
|
||||||
initialMode={getFirstParamValue(resolvedSearchParams.mode)}
|
initialMode={getFirstParamValue(resolvedSearchParams.mode)}
|
||||||
|
initialSiteLogo={loadingBrand.siteLogo}
|
||||||
|
initialSiteLogoDark={loadingBrand.siteLogoDark}
|
||||||
|
initialSiteName={loadingBrand.siteName}
|
||||||
initialWorkspaceType={getFirstParamValue(
|
initialWorkspaceType={getFirstParamValue(
|
||||||
resolvedSearchParams.workspaceType
|
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 (
|
return (
|
||||||
<div className="flex h-dvh w-full items-center justify-center bg-[#151515] text-white/70">
|
<CanvasLogoLoadingView
|
||||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
label="正在加载画布..."
|
||||||
正在加载画布...
|
siteLogo={siteLogo}
|
||||||
</div>
|
siteLogoDark={siteLogoDark}
|
||||||
|
siteName={siteName}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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 (
|
return (
|
||||||
<div className="flex h-dvh w-screen items-center justify-center bg-[#050607] text-white/70">
|
<CanvasLogoLoadingView
|
||||||
<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)]">
|
label="正在进入工作台"
|
||||||
<Loader2 className="h-5 w-5 animate-spin text-emerald-300" />
|
siteLogo={settings?.siteLogo}
|
||||||
<div className="text-sm tracking-[0.08em]">正在进入工作台...</div>
|
siteLogoDark={settings?.siteLogoDark}
|
||||||
</div>
|
siteName={settings?.siteName}
|
||||||
</div>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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-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"
|
: "bg-muted text-muted-foreground hover:bg-muted/80"
|
||||||
: canSend
|
: 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]"
|
: "bg-[#f1f1f2] text-[#b6bac2] hover:bg-[#ececee] dark:bg-[#242529] dark:text-[#777d88] dark:hover:bg-[#2c2d33]"
|
||||||
)}
|
)}
|
||||||
onClick={handleSend}
|
onClick={handleSend}
|
||||||
@ -10899,7 +10899,7 @@ export function CanvasChatPanel({
|
|||||||
canSend &&
|
canSend &&
|
||||||
(isVideoWorkspace || isDetailGenerationMode
|
(isVideoWorkspace || isDetailGenerationMode
|
||||||
? "opacity-95"
|
? "opacity-95"
|
||||||
: "opacity-95 invert dark:invert-0")
|
: "opacity-95")
|
||||||
)}
|
)}
|
||||||
fallback={Sparkles}
|
fallback={Sparkles}
|
||||||
item={getToolbarItem("chatStartGeneration")}
|
item={getToolbarItem("chatStartGeneration")}
|
||||||
@ -10919,7 +10919,7 @@ export function CanvasChatPanel({
|
|||||||
canSend &&
|
canSend &&
|
||||||
(isVideoWorkspace || isDetailGenerationMode
|
(isVideoWorkspace || isDetailGenerationMode
|
||||||
? "opacity-95"
|
? "opacity-95"
|
||||||
: "opacity-95 invert dark:invert-0")
|
: "opacity-95")
|
||||||
)}
|
)}
|
||||||
fallback={ArrowUp}
|
fallback={ArrowUp}
|
||||||
item={getToolbarItem(
|
item={getToolbarItem(
|
||||||
|
|||||||
59
components/canvas/canvas-logo-loading.tsx
Normal file
59
components/canvas/canvas-logo-loading.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -150,24 +150,24 @@ export function LovartHeroComposer({
|
|||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center gap-6 pl-2">
|
<div className="flex h-11 items-center gap-5 pl-2">
|
||||||
<button
|
<button
|
||||||
type="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 模式"
|
aria-label="开启 Agent 模式"
|
||||||
onClick={onStartAgentMode}
|
onClick={onStartAgentMode}
|
||||||
title="开启 Agent 模式"
|
title="开启 Agent 模式"
|
||||||
>
|
>
|
||||||
<Lightbulb className="h-5 w-5" />
|
<Lightbulb className="block h-5 w-5" />
|
||||||
</button>
|
</button>
|
||||||
<Select value={selectedModelId} onValueChange={onModelChange}>
|
<Select value={selectedModelId} onValueChange={onModelChange}>
|
||||||
<SelectTrigger
|
<SelectTrigger
|
||||||
aria-label="选择模型"
|
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="选择模型"
|
title="选择模型"
|
||||||
>
|
>
|
||||||
<span className="inline-flex h-full w-full items-center justify-center">
|
<span className="grid h-full w-full place-items-center">
|
||||||
<Box className="h-5 w-5" />
|
<Box className="block h-5 w-5" />
|
||||||
</span>
|
</span>
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent
|
<SelectContent
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user