feat: add home theme selector and canvas polish
This commit is contained in:
parent
560a1999a0
commit
5b7d6a6e89
@ -9627,10 +9627,10 @@ export function CanvasChatPanel({
|
||||
{message.role === 'user' ? (
|
||||
<div
|
||||
className={cn(
|
||||
"rounded-2xl px-3 py-2",
|
||||
"rounded-2xl border px-3 py-2 shadow-sm",
|
||||
isVideoWorkspace
|
||||
? "border border-border/60 bg-background/80 shadow-sm"
|
||||
: "bg-[#f1f2f4]"
|
||||
? "border-border/60 bg-background/80 text-foreground"
|
||||
: "border-black/5 bg-[#f1f2f4] text-[#1f2937] dark:border-white/10 dark:bg-[#1b1f2a] dark:text-zinc-50"
|
||||
)}
|
||||
>
|
||||
<div className="flex flex-wrap items-center gap-1.5">
|
||||
@ -9653,7 +9653,7 @@ export function CanvasChatPanel({
|
||||
"inline-flex items-center gap-1 rounded-xl px-2 py-1 text-xs",
|
||||
isVideoWorkspace
|
||||
? "border border-border/60 bg-background"
|
||||
: "bg-white/90"
|
||||
: "border border-black/5 bg-white/90 text-[#334155] dark:border-white/10 dark:bg-white/10 dark:text-zinc-100"
|
||||
)}
|
||||
>
|
||||
<img
|
||||
@ -9668,7 +9668,7 @@ export function CanvasChatPanel({
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<span className="max-w-[80px] truncate text-foreground">{part.name || 'Image'}</span>
|
||||
<span className="max-w-[80px] truncate text-current">{part.name || 'Image'}</span>
|
||||
</span>
|
||||
);
|
||||
return;
|
||||
@ -9680,7 +9680,7 @@ export function CanvasChatPanel({
|
||||
.trim();
|
||||
if (!text) return;
|
||||
renderedUserParts.push(
|
||||
<span key={`part-${idx}`} className="text-sm text-foreground">{text}</span>
|
||||
<span key={`part-${idx}`} className="text-sm text-current">{text}</span>
|
||||
);
|
||||
}
|
||||
});
|
||||
@ -9694,7 +9694,7 @@ export function CanvasChatPanel({
|
||||
renderedUserParts.push(
|
||||
<span
|
||||
key={`att-${idx}`}
|
||||
className="inline-flex items-center gap-1 rounded-xl bg-white/90 px-2 py-1 text-xs"
|
||||
className="inline-flex items-center gap-1 rounded-xl border border-black/5 bg-white/90 px-2 py-1 text-xs text-[#334155] dark:border-white/10 dark:bg-white/10 dark:text-zinc-100"
|
||||
>
|
||||
<img
|
||||
src={buildPreviewImageUrl(att.url, 48)}
|
||||
@ -9708,7 +9708,7 @@ export function CanvasChatPanel({
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<span className="max-w-[80px] truncate text-foreground">{att.name || 'Image'}</span>
|
||||
<span className="max-w-[80px] truncate text-current">{att.name || 'Image'}</span>
|
||||
</span>
|
||||
);
|
||||
});
|
||||
@ -9719,7 +9719,7 @@ export function CanvasChatPanel({
|
||||
extractUserTextPrompt(message as any) ||
|
||||
(isVideoModel ? "已发送视频生成请求" : "已发送请求");
|
||||
renderedUserParts.push(
|
||||
<span key="fallback-text" className="text-sm text-foreground">
|
||||
<span key="fallback-text" className="text-sm text-current">
|
||||
{fallbackText}
|
||||
</span>
|
||||
);
|
||||
@ -9733,7 +9733,7 @@ export function CanvasChatPanel({
|
||||
<>
|
||||
<div
|
||||
className={cn(
|
||||
"min-w-0 max-w-full text-[15px] text-[#2f3640]",
|
||||
"min-w-0 max-w-full text-[15px] text-[#2f3640] dark:text-zinc-300",
|
||||
isImageOnlyAssistant
|
||||
? "bg-transparent px-0 py-0"
|
||||
: "bg-transparent px-0 py-0"
|
||||
|
||||
@ -200,6 +200,10 @@ export function World3DNode({
|
||||
() => getNodeContentScale(node.size, { width: 420, height: 320 }, 0.76),
|
||||
[node.size.height, node.size.width]
|
||||
);
|
||||
const overlayChromeScale = useMemo(
|
||||
() => clampNumber(getNodeContentScale(node.size, { width: 420, height: 320 }, 0.48), 0.48, 1),
|
||||
[node.size.height, node.size.width]
|
||||
);
|
||||
const placeholderViewportScale = useMemo(
|
||||
() =>
|
||||
clampNumber(
|
||||
@ -223,6 +227,37 @@ export function World3DNode({
|
||||
}),
|
||||
[placeholderViewportScale]
|
||||
);
|
||||
const topLeftControlsStyle = useMemo<React.CSSProperties>(
|
||||
() => ({
|
||||
transform: `scale(${overlayChromeScale})`,
|
||||
transformOrigin: "top left",
|
||||
}),
|
||||
[overlayChromeScale]
|
||||
);
|
||||
const topRightControlsStyle = useMemo<React.CSSProperties>(
|
||||
() => ({
|
||||
transform: `scale(${overlayChromeScale})`,
|
||||
transformOrigin: "top right",
|
||||
}),
|
||||
[overlayChromeScale]
|
||||
);
|
||||
const bottomOverlayStyle = useMemo<React.CSSProperties>(
|
||||
() => ({
|
||||
paddingLeft: `${12 * overlayChromeScale}px`,
|
||||
paddingRight: `${12 * overlayChromeScale}px`,
|
||||
paddingBottom: `${12 * overlayChromeScale}px`,
|
||||
paddingTop: `${48 * overlayChromeScale}px`,
|
||||
}),
|
||||
[overlayChromeScale]
|
||||
);
|
||||
const bottomOverlayContentStyle = useMemo<React.CSSProperties>(
|
||||
() => ({
|
||||
transform: `scale(${overlayChromeScale})`,
|
||||
transformOrigin: "bottom left",
|
||||
width: `${100 / overlayChromeScale}%`,
|
||||
}),
|
||||
[overlayChromeScale]
|
||||
);
|
||||
const captureEnabled = data.status === "completed" && (activeView === "splat" || activeView === "mesh");
|
||||
|
||||
const handleSetView = (view: World3DViewMode) => {
|
||||
@ -457,8 +492,14 @@ export function World3DNode({
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="pointer-events-none absolute inset-x-0 bottom-0 bg-gradient-to-t from-black/85 via-black/45 to-transparent px-3 pb-3 pt-12">
|
||||
<div className="flex items-end justify-between gap-3">
|
||||
<div
|
||||
className="pointer-events-none absolute inset-x-0 bottom-0 bg-gradient-to-t from-black/85 via-black/45 to-transparent"
|
||||
style={bottomOverlayStyle}
|
||||
>
|
||||
<div
|
||||
className="flex items-end justify-between gap-3"
|
||||
style={bottomOverlayContentStyle}
|
||||
>
|
||||
<div className="min-w-0">
|
||||
<p className="truncate text-sm font-medium text-white">
|
||||
{data.prompt || "3D 世界"}
|
||||
@ -485,7 +526,10 @@ export function World3DNode({
|
||||
</>
|
||||
)}
|
||||
|
||||
<div className="absolute left-3 top-3 z-20 flex items-center gap-2">
|
||||
<div
|
||||
className="absolute left-3 top-3 z-20 flex items-center gap-2"
|
||||
style={topLeftControlsStyle}
|
||||
>
|
||||
<div className="flex rounded-full border border-white/10 bg-black/55 p-1 backdrop-blur-sm">
|
||||
{VIEW_META.filter((view) => availableViews.includes(view.id)).map((view) => (
|
||||
(() => {
|
||||
@ -569,7 +613,10 @@ export function World3DNode({
|
||||
</div>
|
||||
|
||||
{isHovered && data.status === "completed" && (
|
||||
<div className="absolute right-3 top-3 z-20 flex items-center gap-1">
|
||||
<div
|
||||
className="absolute right-3 top-3 z-20 flex items-center gap-1"
|
||||
style={topRightControlsStyle}
|
||||
>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
|
||||
@ -1018,6 +1018,7 @@ function WorldSplatCaptureBridge({
|
||||
|
||||
function WorldSplatViewer({
|
||||
splatUrl,
|
||||
worldUrl,
|
||||
previewImageUrl,
|
||||
firstPerson,
|
||||
hotspots,
|
||||
@ -1029,6 +1030,7 @@ function WorldSplatViewer({
|
||||
onCaptureReady,
|
||||
}: {
|
||||
splatUrl?: string | null;
|
||||
worldUrl?: string | null;
|
||||
previewImageUrl?: string | null;
|
||||
firstPerson?: boolean;
|
||||
hotspots?: World3DHotspot[];
|
||||
@ -1062,6 +1064,21 @@ function WorldSplatViewer({
|
||||
}, [loadState]);
|
||||
|
||||
if (!splatUrl) {
|
||||
if (worldUrl) {
|
||||
return (
|
||||
<div className="h-full w-full overflow-hidden bg-zinc-950">
|
||||
<iframe
|
||||
allow="accelerometer; autoplay; clipboard-read; clipboard-write; fullscreen; gyroscope; xr-spatial-tracking"
|
||||
allowFullScreen
|
||||
className="h-full w-full border-0"
|
||||
referrerPolicy="no-referrer-when-downgrade"
|
||||
src={worldUrl}
|
||||
title="3D 世界在线预览"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<WorldEmptyState
|
||||
description="当前结果没有返回可用的世界数据。"
|
||||
@ -1073,6 +1090,21 @@ function WorldSplatViewer({
|
||||
}
|
||||
|
||||
if (loadState === "error") {
|
||||
if (worldUrl) {
|
||||
return (
|
||||
<div className="h-full w-full overflow-hidden bg-zinc-950">
|
||||
<iframe
|
||||
allow="accelerometer; autoplay; clipboard-read; clipboard-write; fullscreen; gyroscope; xr-spatial-tracking"
|
||||
allowFullScreen
|
||||
className="h-full w-full border-0"
|
||||
referrerPolicy="no-referrer-when-downgrade"
|
||||
src={worldUrl}
|
||||
title="3D 世界在线预览"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<WorldEmptyState
|
||||
description="当前世界数据暂时无法加载,可以切换到图像或 Mesh 视图继续查看。"
|
||||
@ -1223,6 +1255,7 @@ export function World3DViewer({
|
||||
resetViewToken={resetViewToken}
|
||||
semanticsMetadata={semanticsMetadata}
|
||||
splatUrl={splatUrl}
|
||||
worldUrl={worldUrl}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
@ -25,12 +25,14 @@ import {
|
||||
LogOut,
|
||||
Mail,
|
||||
Monitor,
|
||||
Moon,
|
||||
Palette,
|
||||
Plus,
|
||||
PlayCircle,
|
||||
Settings,
|
||||
Sparkles,
|
||||
Star,
|
||||
Sun,
|
||||
Trash2,
|
||||
Upload,
|
||||
User,
|
||||
@ -44,6 +46,13 @@ import {
|
||||
import { toast } from "sonner";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { HoverCard, HoverCardContent, HoverCardTrigger } from "@/components/ui/hover-card";
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
|
||||
import { ImageParamsBar, type ImageGenerationParams } from "@/components/image-params-bar";
|
||||
@ -235,6 +244,34 @@ type UploadFileItem = {
|
||||
previewUrl: string;
|
||||
};
|
||||
|
||||
type ThemeMode = "system" | "light" | "dark";
|
||||
|
||||
const THEME_MODE_OPTIONS: Array<{
|
||||
value: ThemeMode;
|
||||
label: string;
|
||||
description: string;
|
||||
icon: LucideIcon;
|
||||
}> = [
|
||||
{
|
||||
value: "system",
|
||||
label: "跟随系统",
|
||||
description: "随浏览器/系统深浅色自动切换",
|
||||
icon: Monitor,
|
||||
},
|
||||
{
|
||||
value: "light",
|
||||
label: "浅色模式",
|
||||
description: "始终使用明亮界面",
|
||||
icon: Sun,
|
||||
},
|
||||
{
|
||||
value: "dark",
|
||||
label: "深色模式",
|
||||
description: "始终使用暗色界面",
|
||||
icon: Moon,
|
||||
},
|
||||
];
|
||||
|
||||
const DISCOVERY_DEFAULT_TABS = [
|
||||
"品牌设计",
|
||||
"海报与广告",
|
||||
@ -395,7 +432,7 @@ export function LovartHome({
|
||||
heroSubtitleText,
|
||||
}: LovartHomeProps) {
|
||||
const router = useRouter();
|
||||
const { resolvedTheme } = useTheme();
|
||||
const { theme, resolvedTheme, setTheme } = useTheme();
|
||||
const displayLogo =
|
||||
resolvedTheme === "dark"
|
||||
? siteLogoDark?.trim() || siteLogo?.trim() || ""
|
||||
@ -1349,6 +1386,12 @@ export function LovartHome({
|
||||
accountCenterData?.profile?.dailyFreeCredits ?? membershipData?.summary?.dailyFreeCredits ?? 0,
|
||||
};
|
||||
}, [accountCenterData?.profile, currentPlan?.monthlyCredits, currentPlan?.name, membershipData?.summary?.currentCredits, membershipData?.summary?.dailyFreeCredits, user?.email, userDisplayName]);
|
||||
const selectedThemeMode: ThemeMode =
|
||||
theme === "light" || theme === "dark" || theme === "system" ? theme : "system";
|
||||
const selectedThemeOption =
|
||||
THEME_MODE_OPTIONS.find((option) => option.value === selectedThemeMode) ||
|
||||
THEME_MODE_OPTIONS[0];
|
||||
const SelectedThemeIcon = selectedThemeOption.icon;
|
||||
|
||||
return (
|
||||
<div className="min-h-svh bg-[#f4f4f4] text-[#171717] dark:bg-[#0a0a0c] dark:text-zinc-100 [&_button]:cursor-pointer [&_input]:cursor-pointer [&_textarea]:cursor-pointer [&_img]:cursor-pointer [&_a]:cursor-pointer [&_[role=button]]:cursor-pointer">
|
||||
@ -1444,6 +1487,73 @@ export function LovartHome({
|
||||
</HoverCard>
|
||||
) : null}
|
||||
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
className="inline-flex h-8 items-center gap-1.5 rounded-[14px] border border-black/10 bg-white px-2.5 text-[12px] font-medium text-black/70 shadow-sm transition hover:bg-black/5 hover:text-black dark:border-white/10 dark:bg-white/6 dark:text-white/72 dark:hover:bg-white/10 dark:hover:text-white"
|
||||
title={`主题模式:${selectedThemeOption.label}`}
|
||||
aria-label={`主题模式:${selectedThemeOption.label}`}
|
||||
>
|
||||
<SelectedThemeIcon className="h-3.5 w-3.5" />
|
||||
<span className="hidden sm:inline">{selectedThemeOption.label}</span>
|
||||
<ChevronDown className="h-3.5 w-3.5 opacity-55" />
|
||||
</button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
align="end"
|
||||
sideOffset={10}
|
||||
className="w-[220px] rounded-2xl border-black/10 bg-white p-1.5 shadow-xl dark:border-white/10 dark:bg-[#141416]"
|
||||
>
|
||||
<DropdownMenuLabel className="px-3 py-2 text-[12px] text-black/45 dark:text-white/45">
|
||||
个人主题模式
|
||||
</DropdownMenuLabel>
|
||||
{THEME_MODE_OPTIONS.map((option) => {
|
||||
const OptionIcon = option.icon;
|
||||
const isSelected = selectedThemeMode === option.value;
|
||||
|
||||
return (
|
||||
<DropdownMenuItem
|
||||
key={option.value}
|
||||
className={cn(
|
||||
"flex cursor-pointer items-center gap-3 rounded-xl px-3 py-2 text-left focus:bg-black/5 dark:focus:bg-white/8",
|
||||
isSelected
|
||||
? "bg-black text-white focus:bg-black focus:text-white dark:bg-white dark:text-black dark:focus:bg-white dark:focus:text-black"
|
||||
: "text-black/78 dark:text-white/78"
|
||||
)}
|
||||
onSelect={() => setTheme(option.value)}
|
||||
>
|
||||
<span
|
||||
className={cn(
|
||||
"flex h-8 w-8 items-center justify-center rounded-xl",
|
||||
isSelected
|
||||
? "bg-white/14 text-white dark:bg-black/8 dark:text-black"
|
||||
: "bg-black/5 text-black/65 dark:bg-white/8 dark:text-white/65"
|
||||
)}
|
||||
>
|
||||
<OptionIcon className="h-4 w-4" />
|
||||
</span>
|
||||
<span className="min-w-0 flex-1">
|
||||
<span className="block text-[13px] font-semibold leading-4">
|
||||
{option.label}
|
||||
</span>
|
||||
<span
|
||||
className={cn(
|
||||
"mt-0.5 block text-[11px] leading-4",
|
||||
isSelected
|
||||
? "text-white/70 dark:text-black/60"
|
||||
: "text-black/45 dark:text-white/42"
|
||||
)}
|
||||
>
|
||||
{option.description}
|
||||
</span>
|
||||
</span>
|
||||
</DropdownMenuItem>
|
||||
);
|
||||
})}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
|
||||
{isAuthenticated ? (
|
||||
<HoverCard openDelay={80} closeDelay={120}>
|
||||
<HoverCardTrigger asChild>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user