fix: refine Lovart-style canvas and home UI
This commit is contained in:
parent
6d7f869e56
commit
f02d18702a
@ -2071,7 +2071,7 @@ export function AdminSettingsClient() {
|
||||
usageTimeMessage: "由壹点累了也是需要休息的",
|
||||
defaultTheme: "system",
|
||||
bannerEnabled: true,
|
||||
bannerText: "升级会员, Nano Banana Pro 免费365天!",
|
||||
bannerText: "限时特惠:升级会员最高立享 45% OFF!",
|
||||
bannerLink: null,
|
||||
toolbox1Name: "工具箱",
|
||||
toolbox1Enabled: true,
|
||||
@ -2108,7 +2108,7 @@ export function AdminSettingsClient() {
|
||||
agentWorkflowDefaultModelId: null,
|
||||
canvasDetailDefaultModelId: null,
|
||||
canvasDetailAgentModelId: null,
|
||||
lovartComposerPrefix: "让Buyi-ai",
|
||||
lovartComposerPrefix: "让「由壹点AI」",
|
||||
lovartComposerPreviewTexts: [
|
||||
"打造引人注目的社交媒体视觉",
|
||||
"帮我把这张产品图做成高级电商海报",
|
||||
@ -2181,7 +2181,7 @@ export function AdminSettingsClient() {
|
||||
defaultTheme: siteSettingsData.defaultTheme || "system",
|
||||
bannerEnabled: siteSettingsData.bannerEnabled ?? true,
|
||||
bannerText:
|
||||
siteSettingsData.bannerText || "升级会员, Nano Banana Pro 免费365天!",
|
||||
siteSettingsData.bannerText || "限时特惠:升级会员最高立享 45% OFF!",
|
||||
bannerLink: siteSettingsData.bannerLink || null,
|
||||
toolbox1Name: siteSettingsData.toolbox1Name || "工具箱",
|
||||
toolbox1Enabled: siteSettingsData.toolbox1Enabled ?? true,
|
||||
@ -2269,7 +2269,7 @@ export function AdminSettingsClient() {
|
||||
(siteSettingsData as any).canvasDetailAgentModelId || null,
|
||||
lovartComposerPrefix:
|
||||
String((siteSettingsData as any).lovartComposerPrefix || "").trim() ||
|
||||
"让Buyi-ai",
|
||||
"让「由壹点AI」",
|
||||
lovartComposerPreviewTexts: Array.isArray(
|
||||
siteSettingsData.lovartComposerPreviewTexts
|
||||
)
|
||||
@ -9905,7 +9905,7 @@ export function AdminSettingsClient() {
|
||||
lovartComposerPrefix: e.target.value.slice(0, 40),
|
||||
})
|
||||
}
|
||||
placeholder="让Buyi-ai"
|
||||
placeholder="让「由壹点AI」"
|
||||
value={siteSettings.lovartComposerPrefix}
|
||||
/>
|
||||
<div className="text-muted-foreground text-xs">
|
||||
|
||||
@ -382,7 +382,7 @@ function normalizeLovartComposerPreviewTexts(value: unknown): string[] {
|
||||
function normalizeLovartComposerPrefix(value: unknown): string {
|
||||
const normalized = String(value || "").trim();
|
||||
if (!normalized) {
|
||||
return "让Buyi-ai";
|
||||
return "让「由壹点AI」";
|
||||
}
|
||||
return normalized.slice(0, 40);
|
||||
}
|
||||
@ -538,7 +538,7 @@ const HOME_SITE_SETTINGS_FALLBACK: Record<string, unknown> = {
|
||||
lovartHeroSubtitle: "懂你的设计代理,帮你搞定一切",
|
||||
surpriseCategories: ["全部", "精选"],
|
||||
canvasDefaultModelId: null,
|
||||
lovartComposerPrefix: "让Buyi-ai",
|
||||
lovartComposerPrefix: "让「由壹点AI」",
|
||||
lovartComposerPreviewTexts: [],
|
||||
lovartPromptTagVisibility: {},
|
||||
contactConfig: normalizeContactConfig(undefined),
|
||||
@ -844,7 +844,7 @@ async function getScopedPublicSiteSettings(
|
||||
...createBrandingSettings(typedSettings),
|
||||
bannerEnabled: typedSettings?.bannerEnabled ?? true,
|
||||
bannerText:
|
||||
typedSettings?.bannerText || "升级会员, Nano Banana Pro 免费365天!",
|
||||
typedSettings?.bannerText || "限时特惠:升级会员最高立享 45% OFF!",
|
||||
bannerLink: typedSettings?.bannerLink || null,
|
||||
lovartComposerPrefix: normalizeLovartComposerPrefix(
|
||||
valueByKey.get(LOVART_COMPOSER_PREFIX_KEY)
|
||||
@ -1376,7 +1376,7 @@ export async function GET(request: NextRequest) {
|
||||
usageTimeEnd: "20:00",
|
||||
usageTimeMessage: "由壹点累了也是需要休息的",
|
||||
bannerEnabled: true,
|
||||
bannerText: "升级会员, Nano Banana Pro 免费365天!",
|
||||
bannerText: "限时特惠:升级会员最高立享 45% OFF!",
|
||||
bannerLink: null,
|
||||
toolbox1Name: "工具箱",
|
||||
toolbox1Enabled: true,
|
||||
@ -2959,7 +2959,7 @@ export async function PUT(request: NextRequest) {
|
||||
usageTimeEnd: siteSettingsPayload.usageTimeEnd ?? "20:00",
|
||||
usageTimeMessage: siteSettingsPayload.usageTimeMessage ?? "由壹点累了也是需要休息的",
|
||||
bannerEnabled: siteSettingsPayload.bannerEnabled ?? true,
|
||||
bannerText: siteSettingsPayload.bannerText ?? "升级会员, Nano Banana Pro 免费365天!",
|
||||
bannerText: siteSettingsPayload.bannerText ?? "限时特惠:升级会员最高立享 45% OFF!",
|
||||
bannerLink: siteSettingsPayload.bannerLink,
|
||||
toolbox1Name: siteSettingsPayload.toolbox1Name ?? "工具箱",
|
||||
toolbox1Enabled: siteSettingsPayload.toolbox1Enabled ?? true,
|
||||
|
||||
@ -57,6 +57,7 @@ import {
|
||||
ZoomOut,
|
||||
} from "lucide-react";
|
||||
import dynamic from "next/dynamic";
|
||||
import { useTheme } from "next-themes";
|
||||
import { useParams, useRouter, useSearchParams } from "next/navigation";
|
||||
import {
|
||||
type ChangeEvent,
|
||||
@ -244,6 +245,7 @@ import type {
|
||||
|
||||
const CANVAS_SITE_SETTINGS_REFRESH_INTERVAL_MS = 15000;
|
||||
const CANVAS_SITE_SETTINGS_TIMEOUT_MS = 10000;
|
||||
const CANVAS_SURFACE_BACKGROUND = "#f4f4f5";
|
||||
|
||||
const fetchCanvasSiteSettings = async (url: string) => {
|
||||
const separator = url.includes("?") ? "&" : "?";
|
||||
@ -1122,7 +1124,7 @@ function normalizeImportedCanvasState(payload: unknown): {
|
||||
};
|
||||
}
|
||||
|
||||
const FLOATING_PANEL_WIDTH = 460;
|
||||
const FLOATING_PANEL_WIDTH = 400;
|
||||
const FLOATING_PANEL_INSET = 16;
|
||||
const DOCKED_PANEL_INSET = 0;
|
||||
const FLOATING_UI_GAP = 16;
|
||||
@ -1710,6 +1712,7 @@ export default function CanvasEditorLegacyPage({ params }: PageProps) {
|
||||
? routeId
|
||||
: "";
|
||||
const router = useRouter();
|
||||
const { resolvedTheme } = useTheme();
|
||||
const mode = searchParams.get("mode");
|
||||
const requestedWorkspaceType = searchParams.get("workspaceType");
|
||||
const requestedAgentMode = mode === "agent";
|
||||
@ -1719,6 +1722,8 @@ export default function CanvasEditorLegacyPage({ params }: PageProps) {
|
||||
const activeCanvasWorkspaceType: CanvasWorkspaceType = isVideoMode
|
||||
? "video"
|
||||
: "canvas";
|
||||
const canvasSurfaceBackground =
|
||||
resolvedTheme === "dark" ? "#0a0a0c" : CANVAS_SURFACE_BACKGROUND;
|
||||
const defaultCanvasTitle = isVideoMode ? "未命名视频画布" : "未命名画布";
|
||||
const workspaceLabel = isVideoMode ? "视频画布" : "画布";
|
||||
const createNewCanvasUrl = isVideoMode
|
||||
@ -8952,7 +8957,7 @@ export default function CanvasEditorLegacyPage({ params }: PageProps) {
|
||||
const brandEntry = (
|
||||
<div
|
||||
className="absolute top-4 left-4 z-20 rounded-full px-1 py-1 shadow-none"
|
||||
style={{ backgroundColor: "var(--background)" }}
|
||||
style={{ backgroundColor: canvasSurfaceBackground }}
|
||||
>
|
||||
<Button
|
||||
className="h-9 gap-2 rounded-full border-0 bg-transparent px-2.5 shadow-none hover:bg-transparent hover:text-foreground"
|
||||
@ -9365,7 +9370,15 @@ export default function CanvasEditorLegacyPage({ params }: PageProps) {
|
||||
|
||||
return (
|
||||
<TooltipProvider>
|
||||
<div className="relative h-dvh w-full overflow-hidden overscroll-none bg-background">
|
||||
<div
|
||||
className="relative h-dvh w-full overflow-hidden overscroll-none"
|
||||
style={
|
||||
{
|
||||
"--canvas-surface-background": canvasSurfaceBackground,
|
||||
backgroundColor: canvasSurfaceBackground,
|
||||
} as React.CSSProperties
|
||||
}
|
||||
>
|
||||
{/* 1. Canvas Layer (Full Screen) - 使用 key 强制在 id 变化时重新挂载 */}
|
||||
<div
|
||||
className="absolute inset-0 z-0"
|
||||
|
||||
@ -24,9 +24,8 @@ import {
|
||||
type UiSurfaceSnapshot,
|
||||
} from "@/lib/ui-surface-protocol";
|
||||
import useSWR, { useSWRConfig } from "swr";
|
||||
import { Send, Loader2, ImagePlus, Paperclip, X, Plus, History, LayoutGrid, ArrowRightToLine, RefreshCw, Globe, Box, ArrowUp, Video, Sparkles, Check, ChevronDown, ChevronUp, AlertCircle, CheckCircle2, Brain, ThumbsUp, ThumbsDown, Eye, Trash2, Download, MessageCircle, Clapperboard, Bot } from "lucide-react";
|
||||
import { Loader2, ImagePlus, X, Plus, History, LayoutGrid, ArrowRightToLine, RefreshCw, Globe, ArrowUp, Video, Sparkles, Check, ChevronDown, ChevronUp, AlertCircle, CheckCircle2, Brain, ThumbsUp, ThumbsDown, Eye, Trash2, Download, MessageCircle, Clapperboard, Bot } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import {
|
||||
Collapsible,
|
||||
CollapsibleContent,
|
||||
@ -349,7 +348,7 @@ type VideoStoryboardStreamData =
|
||||
|
||||
// 建议卡片数据 - 已移除
|
||||
const SUGGESTIONS: { title: string; desc: string; image: string }[] = [];
|
||||
const CANVAS_COMPOSER_PREFIX_DEFAULT = "让Buyi-ai";
|
||||
const CANVAS_COMPOSER_PREFIX_DEFAULT = "让「由壹点AI」";
|
||||
const CANVAS_COMPOSER_PREVIEW_TEXTS_DEFAULT = [
|
||||
"打造引人注目的社交媒体视觉",
|
||||
"帮我把这张产品图做成高级电商海报",
|
||||
@ -972,6 +971,7 @@ export function CanvasChatPanel({
|
||||
const inFlightRawChatIdRef = useRef<string | null>(null);
|
||||
const [chatSessions, setChatSessions] = useState<CanvasChatSession[]>([]);
|
||||
const [isSessionMenuOpen, setIsSessionMenuOpen] = useState(false);
|
||||
const [isComposerModeMenuOpen, setIsComposerModeMenuOpen] = useState(false);
|
||||
const [selectedSkillTagId, setSelectedSkillTagId] = useState<string | null>(null);
|
||||
const [messageViewportKey, setMessageViewportKey] = useState(0);
|
||||
const messagesEndRef = useRef<HTMLDivElement>(null);
|
||||
@ -2031,6 +2031,7 @@ export function CanvasChatPanel({
|
||||
const [canvasTypedPreviewText, setCanvasTypedPreviewText] = useState("");
|
||||
const [canvasPreviewCursorVisible, setCanvasPreviewCursorVisible] = useState(true);
|
||||
const [greetingPeriodText, setGreetingPeriodText] = useState("中午好!");
|
||||
const [isCanvasBannerDismissed, setIsCanvasBannerDismissed] = useState(false);
|
||||
useEffect(() => {
|
||||
const hour = new Date().getHours();
|
||||
if (hour >= 5 && hour < 11) {
|
||||
@ -2141,8 +2142,12 @@ export function CanvasChatPanel({
|
||||
const canUseDetailGeneration = aiDetailGenerationEnabled && !isVideoWorkspace;
|
||||
const shouldShowCanvasBanner =
|
||||
!isDetailGenerationMode &&
|
||||
!isCanvasBannerDismissed &&
|
||||
(siteSettings?.bannerEnabled ?? true) &&
|
||||
Boolean(siteSettings?.bannerText || "升级会员, Nano Banana Pro 免费365天!");
|
||||
Boolean(siteSettings?.bannerText || "限时特惠:升级会员最高立享 45% OFF!");
|
||||
useEffect(() => {
|
||||
setIsCanvasBannerDismissed(false);
|
||||
}, [siteSettings?.bannerEnabled, siteSettings?.bannerText]);
|
||||
const canvasSkillTagsEnabled = canvasSkillTagsData?.enabled !== false;
|
||||
const canvasSkillTagsTitle =
|
||||
String(canvasSkillTagsData?.title || "").trim() || DEFAULT_CANVAS_SKILL_TAGS_TITLE;
|
||||
@ -2730,14 +2735,14 @@ export function CanvasChatPanel({
|
||||
);
|
||||
const activeModeLabel =
|
||||
activeComposerMode === "video"
|
||||
? getToolbarItem("chatModeVideoActive").label
|
||||
? "视频"
|
||||
: activeComposerMode === "detail"
|
||||
? getToolbarItem("chatModeDetailActive").label
|
||||
: activeComposerMode === "image"
|
||||
? getToolbarItem("chatModeImageActive").label
|
||||
? "图像"
|
||||
: isPresetAgentSelection
|
||||
? getToolbarItem("chatModePresetAgent").label
|
||||
: getToolbarItem("chatModeAgentActive").label;
|
||||
: "Agent";
|
||||
const shouldMaskAgentRuntimeModelName =
|
||||
activeComposerMode === "agent" || selectedModelId === "auto";
|
||||
const getAgentSafeModelName = useCallback(
|
||||
@ -3963,9 +3968,11 @@ export function CanvasChatPanel({
|
||||
|
||||
setMessages(sortCanvasMessagesByCreatedAt(dbMessages));
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
messagesEndRef.current?.scrollIntoView({ behavior: "auto", block: "end" });
|
||||
});
|
||||
if (dbMessages.length > 0) {
|
||||
requestAnimationFrame(() => {
|
||||
messagesEndRef.current?.scrollIntoView({ behavior: "auto", block: "end" });
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const reloadMessagesOnce = async () => {
|
||||
@ -4149,19 +4156,21 @@ export function CanvasChatPanel({
|
||||
setMessageViewportKey((prev) => prev + 1);
|
||||
console.log("[CanvasChat] 已同步历史消息:", historyMessages.length, signature);
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
messagesEndRef.current?.scrollIntoView({ behavior: "auto", block: "end" });
|
||||
});
|
||||
historyScrollTimerRef.current = setTimeout(() => {
|
||||
messagesEndRef.current?.scrollIntoView({ behavior: "auto", block: "end" });
|
||||
historyScrollTimerRef.current = null;
|
||||
}, 160);
|
||||
return () => {
|
||||
if (historyScrollTimerRef.current) {
|
||||
clearTimeout(historyScrollTimerRef.current);
|
||||
if (historyMessages.length > 0) {
|
||||
requestAnimationFrame(() => {
|
||||
messagesEndRef.current?.scrollIntoView({ behavior: "auto", block: "end" });
|
||||
});
|
||||
historyScrollTimerRef.current = setTimeout(() => {
|
||||
messagesEndRef.current?.scrollIntoView({ behavior: "auto", block: "end" });
|
||||
historyScrollTimerRef.current = null;
|
||||
}
|
||||
};
|
||||
}, 160);
|
||||
return () => {
|
||||
if (historyScrollTimerRef.current) {
|
||||
clearTimeout(historyScrollTimerRef.current);
|
||||
historyScrollTimerRef.current = null;
|
||||
}
|
||||
};
|
||||
}
|
||||
}, [historyData, rawChatId, setMessages, status]);
|
||||
|
||||
useEffect(() => {
|
||||
@ -4625,6 +4634,7 @@ export function CanvasChatPanel({
|
||||
|
||||
// 滚动到底部
|
||||
useEffect(() => {
|
||||
if (messages.length === 0) return;
|
||||
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
|
||||
}, [messages]);
|
||||
|
||||
@ -7793,7 +7803,7 @@ export function CanvasChatPanel({
|
||||
: selectedSkillTag?.placeholder?.trim() ||
|
||||
(selectedSkillTag
|
||||
? `请输入「${selectedSkillTag.name}」相关需求`
|
||||
: canvasDynamicPlaceholder || "请输入你的设计需求,例如:绿色椅子 替换 Image");
|
||||
: "请输入你的设计需求");
|
||||
const detailProductCount = DETAIL_PRODUCT_SLOT_INDEXES.filter((index) => Boolean(detailSlots[index]?.url)).length;
|
||||
const detailModelReady = DETAIL_MODEL_SLOT_INDEX >= 0 ? Boolean(detailSlots[DETAIL_MODEL_SLOT_INDEX]?.url) : false;
|
||||
const detailBundleCount = DETAIL_BUNDLE_SLOT_INDEXES.filter((index) => Boolean(detailSlots[index]?.url)).length;
|
||||
@ -7915,6 +7925,13 @@ export function CanvasChatPanel({
|
||||
renderableVisibleMessages.length === 0 &&
|
||||
!isLoading &&
|
||||
!shouldShowImageLoading;
|
||||
const isPlainCanvasEmptyState =
|
||||
!isWorkspacePresentation &&
|
||||
!isDetailGenerationMode &&
|
||||
!isVideoWorkspace &&
|
||||
renderableVisibleMessages.length === 0 &&
|
||||
!isLoading &&
|
||||
!shouldShowImageLoading;
|
||||
const workspaceEmptyTitle =
|
||||
activeComposerMode === "agent" && !isPresetAgentSelection
|
||||
? "开始 Auto Agent 对话"
|
||||
@ -7991,13 +8008,13 @@ export function CanvasChatPanel({
|
||||
const renderModelSelectContent = () => (
|
||||
<SelectContent
|
||||
className={cn(
|
||||
"w-[238px] max-h-[340px] overflow-hidden p-1.5",
|
||||
"w-[170px] max-h-[320px] overflow-hidden rounded-[14px] border border-[#cfd4dc] bg-white p-0 text-[#252b33] shadow-[0_18px_44px_rgba(15,23,42,0.16)]",
|
||||
isVideoWorkspace
|
||||
? "rounded-[18px] border border-border/60 bg-background/95 shadow-[0_18px_40px_-24px_rgba(15,23,42,0.35)] backdrop-blur-xl"
|
||||
: "rounded-[18px] border border-[#cfd4dc] bg-[#f3f4f6]/90 shadow-[0_8px_20px_rgba(15,23,42,0.13)] dark:border-white/10 dark:bg-zinc-900/95"
|
||||
? "rounded-[14px] border border-border/60 bg-background/95 shadow-[0_14px_30px_-20px_rgba(15,23,42,0.35)] backdrop-blur-xl"
|
||||
: "dark:border-white/10 dark:bg-zinc-900/95 dark:text-zinc-100"
|
||||
)}
|
||||
>
|
||||
<div className="max-h-[312px] space-y-0.5 overflow-y-auto pr-1">
|
||||
<div className="max-h-[304px] space-y-0.5 overflow-y-auto p-1.5">
|
||||
{selectableModels.map((model) => {
|
||||
const discountLabel = getImageModelCreditDiscountLabel(
|
||||
model.imageGenConfig
|
||||
@ -8009,10 +8026,10 @@ export function CanvasChatPanel({
|
||||
value={model.id}
|
||||
disabled={model.selectable === false}
|
||||
className={cn(
|
||||
"h-[34px] rounded-xl px-2.5 [&>span:first-child]:hidden [&>span:last-child]:block [&>span:last-child]:w-full",
|
||||
"h-8 rounded-[10px] px-2 text-[13px] font-medium [&>span:first-child]:hidden [&>span:last-child]:block [&>span:last-child]:w-full",
|
||||
isVideoWorkspace
|
||||
? "text-foreground focus:bg-background focus:text-foreground data-[state=checked]:bg-background data-[state=checked]:text-foreground"
|
||||
: "text-[#2f3640] focus:bg-[#e7ebf0] focus:text-[#1f2937] data-[state=checked]:bg-[#e7ebf0] data-[state=checked]:text-[#1f2937] dark:text-zinc-100 dark:focus:bg-white/10 dark:focus:text-white dark:data-[state=checked]:bg-white/10 dark:data-[state=checked]:text-white",
|
||||
: "text-[#2f3640] focus:bg-[#f4f5f6] focus:text-[#1f2937] data-[state=checked]:bg-transparent data-[state=checked]:text-[#1f2937] dark:text-zinc-100 dark:focus:bg-white/10 dark:focus:text-white dark:data-[state=checked]:text-white",
|
||||
model.selectable === false && "opacity-55"
|
||||
)}
|
||||
>
|
||||
@ -8020,10 +8037,10 @@ export function CanvasChatPanel({
|
||||
<span className="flex min-w-0 items-center gap-2">
|
||||
<span
|
||||
className={cn(
|
||||
"h-5 w-5 overflow-hidden rounded-full",
|
||||
"h-4 w-4 overflow-hidden rounded-full",
|
||||
isVideoWorkspace
|
||||
? "border border-border/60 bg-background"
|
||||
: "border border-[#d4d9df] bg-white dark:border-white/20 dark:bg-white"
|
||||
: "border border-transparent bg-white dark:border-white/20 dark:bg-white"
|
||||
)}
|
||||
>
|
||||
{model.avatarUrl ? (
|
||||
@ -8050,13 +8067,13 @@ export function CanvasChatPanel({
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
<span className="truncate text-[12px] font-medium leading-none">{model.displayName}</span>
|
||||
<span className="truncate text-[13px] font-medium leading-none">{model.displayName}</span>
|
||||
</span>
|
||||
<span className="flex shrink-0 items-center gap-1.5">
|
||||
<span className="flex shrink-0 items-center gap-1">
|
||||
{discountLabel ? (
|
||||
<span
|
||||
className={cn(
|
||||
"rounded-md border px-1.5 py-0.5 text-[10px] font-semibold leading-none",
|
||||
"rounded-[5px] border px-1 py-0.5 text-[8px] font-semibold leading-none",
|
||||
isVideoWorkspace
|
||||
? "border-border/70 bg-muted/80 text-foreground/80"
|
||||
: "border-[#d0d4dc] bg-[#eef1f5] text-[#5c6574] dark:border-white/15 dark:bg-white/10 dark:text-zinc-200"
|
||||
@ -8068,7 +8085,7 @@ export function CanvasChatPanel({
|
||||
{model.availabilityLabel ? (
|
||||
<span
|
||||
className={cn(
|
||||
"rounded-md border px-1.5 py-0.5 text-[10px] font-semibold leading-none",
|
||||
"rounded-[5px] border px-1 py-0.5 text-[8px] font-semibold leading-none",
|
||||
model.availabilityStatus === "maintenance"
|
||||
? isVideoWorkspace
|
||||
? "border-amber-400/30 bg-amber-500/10 text-amber-300"
|
||||
@ -8082,14 +8099,7 @@ export function CanvasChatPanel({
|
||||
</span>
|
||||
) : null}
|
||||
{modelSelectValue === model.id ? (
|
||||
<span
|
||||
className={cn(
|
||||
"text-sm font-semibold",
|
||||
isVideoWorkspace ? "text-foreground" : "text-[#1f2937]"
|
||||
)}
|
||||
>
|
||||
✓
|
||||
</span>
|
||||
<Check className={cn("h-3.5 w-3.5", isVideoWorkspace ? "text-foreground" : "text-[#1f2937]")} />
|
||||
) : null}
|
||||
</span>
|
||||
</span>
|
||||
@ -8099,7 +8109,7 @@ export function CanvasChatPanel({
|
||||
{selectableModels.length === 0 ? (
|
||||
<div
|
||||
className={cn(
|
||||
"px-2 py-2 text-xs",
|
||||
"flex h-9 items-center px-2 text-[12px]",
|
||||
isVideoWorkspace ? "text-muted-foreground" : "text-[#6b7280]"
|
||||
)}
|
||||
>
|
||||
@ -8341,14 +8351,20 @@ export function CanvasChatPanel({
|
||||
<div
|
||||
key={`${activeChatId}-${messageViewportKey}`}
|
||||
className={cn(
|
||||
"flex-1 min-h-0 overflow-y-auto overflow-x-hidden",
|
||||
"flex-1 min-h-0 overflow-x-hidden",
|
||||
isPlainCanvasEmptyState ? "overflow-hidden" : "overflow-y-auto",
|
||||
isWorkspacePresentation ? "p-6" : "p-4",
|
||||
isDetailGenerationMode && renderableVisibleMessages.length === 0
|
||||
? "pt-10"
|
||||
: "pt-4"
|
||||
isPlainCanvasEmptyState
|
||||
? "pt-0"
|
||||
: isDetailGenerationMode && renderableVisibleMessages.length === 0
|
||||
? "pt-10"
|
||||
: "pt-4"
|
||||
)}
|
||||
>
|
||||
<div className="space-y-6 overflow-x-hidden pb-24 pr-1">
|
||||
<div className={cn(
|
||||
"space-y-6 overflow-x-hidden pr-1",
|
||||
isPlainCanvasEmptyState ? "h-full pb-0" : "pb-24"
|
||||
)}>
|
||||
{!isDetailGenerationMode && renderableVisibleMessages.length === 0 ? (
|
||||
isWorkspacePresentation ? (
|
||||
<div className="flex h-full min-h-[360px] items-center justify-center">
|
||||
@ -8390,7 +8406,13 @@ export function CanvasChatPanel({
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className={cn("flex flex-col h-full", isDetailGenerationMode ? "pt-0" : "pt-4")}>
|
||||
<div
|
||||
className={cn(
|
||||
"flex h-full flex-col",
|
||||
isPlainCanvasEmptyState ? "min-h-0" : "min-h-[360px]",
|
||||
isDetailGenerationMode ? "pt-0" : "pt-4"
|
||||
)}
|
||||
>
|
||||
{isVideoWorkspace ? (
|
||||
<div className="mb-8 rounded-[30px] border border-border/60 bg-background/82 p-5 shadow-[0_22px_50px_-32px_rgba(15,23,42,0.35)] backdrop-blur-xl">
|
||||
<div className="flex items-start gap-3">
|
||||
@ -8460,41 +8482,31 @@ export function CanvasChatPanel({
|
||||
) : null}
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div className={cn(isDetailGenerationMode ? "mb-4" : "mb-8")}>
|
||||
<h1 className="mb-2 text-2xl font-bold">
|
||||
<span>Hi</span>
|
||||
<div className="flex flex-1 flex-col px-7 pt-12 pb-12">
|
||||
<div className="text-left">
|
||||
<h1 className="text-[24px] font-semibold leading-tight tracking-tight text-[#050505] dark:text-zinc-50">
|
||||
Hi,{" "}
|
||||
{greetingUserName ? (
|
||||
<>
|
||||
<span>, </span>
|
||||
<span className="text-blue-600">{greetingUserName}</span>
|
||||
<span> {greetingPeriodText}</span>
|
||||
</>
|
||||
) : (
|
||||
<span> {greetingPeriodText}</span>
|
||||
)}
|
||||
<span className="text-[#2563ff]">{greetingUserName}</span>
|
||||
) : null}{" "}
|
||||
{greetingPeriodText}
|
||||
</h1>
|
||||
<p className="text-muted-foreground">
|
||||
开始创作吧!
|
||||
{Number.isFinite(totalGeneratedImages) ? (
|
||||
<>
|
||||
{" "}累计为你生成了{" "}
|
||||
<span className="text-blue-600 font-semibold">
|
||||
{totalGeneratedImages.toLocaleString()}
|
||||
</span>{" "}
|
||||
张图。
|
||||
</>
|
||||
) : null}
|
||||
<p className="mt-3 text-[15px] font-normal leading-6 text-[#6b7280] dark:text-zinc-400">
|
||||
开始创作吧! 累计为你生成了{" "}
|
||||
<span className="font-semibold text-[#2563ff]">
|
||||
{Number.isFinite(totalGeneratedImages) ? totalGeneratedImages : 0}
|
||||
</span>{" "}
|
||||
张图。
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{canvasSkillTags.length > 0 && (
|
||||
<div className="mb-8 rounded-3xl border border-[#eceef2] bg-white/90 p-4 dark:border-white/10 dark:bg-zinc-900/75">
|
||||
<div className="mb-3 text-center">
|
||||
<h3 className="text-[16px] font-semibold text-[#1f2937] dark:text-zinc-100">{canvasSkillTagsTitle}</h3>
|
||||
</div>
|
||||
<div className="grid grid-cols-3 gap-2">
|
||||
{canvasSkillTags.map((tag) => {
|
||||
{canvasSkillTags.length > 0 ? (
|
||||
<div className="mt-8 rounded-[26px] border border-[#e4e7ec] bg-white px-5 py-6 text-center dark:border-white/10 dark:bg-zinc-950/70">
|
||||
<h3 className="text-[16px] font-semibold leading-none text-[#1f2328] dark:text-zinc-100">
|
||||
预设快捷功能
|
||||
</h3>
|
||||
<div className="mt-6 flex flex-wrap justify-center gap-2">
|
||||
{canvasSkillTags.slice(0, 4).map((tag) => {
|
||||
const isActive = selectedSkillTag?.id === tag.id;
|
||||
const TagIcon = getCanvasSkillIconComponent(tag.iconName);
|
||||
return (
|
||||
@ -8507,17 +8519,17 @@ export function CanvasChatPanel({
|
||||
editor?.focus();
|
||||
}}
|
||||
className={cn(
|
||||
"inline-flex min-w-0 items-center justify-center gap-1.5 rounded-2xl border px-2 py-1.5 text-[13px] font-medium transition-colors",
|
||||
"inline-flex h-[34px] max-w-full items-center justify-center gap-1.5 rounded-full border px-3.5 text-[13px] font-medium leading-none transition-colors",
|
||||
isActive
|
||||
? "border-[#6d7fff] bg-[#eef1ff] text-[#3341a9] dark:border-blue-400/40 dark:bg-blue-500/15 dark:text-blue-200"
|
||||
: "border-[#e4e7ec] bg-white text-[#30343b] hover:border-[#d6dae3] hover:bg-[#fafbfc] dark:border-white/10 dark:bg-white/5 dark:text-zinc-200 dark:hover:border-white/20 dark:hover:bg-white/10"
|
||||
? "border-[#d8dce4] bg-[#f7f8fa] text-[#1f2328] dark:border-blue-400/40 dark:bg-blue-500/15 dark:text-blue-200"
|
||||
: "border-[#e5e7eb] bg-white text-[#1f2328] hover:border-[#d6dae3] hover:bg-[#fafbfc] dark:border-white/10 dark:bg-white/5 dark:text-zinc-200 dark:hover:border-white/20 dark:hover:bg-white/10"
|
||||
)}
|
||||
>
|
||||
{tag.iconUrl ? (
|
||||
<img
|
||||
src={buildPreviewImageUrl(tag.iconUrl, 48)}
|
||||
src={buildPreviewImageUrl(tag.iconUrl, 40)}
|
||||
alt={tag.name}
|
||||
className="h-3.5 w-3.5 rounded-sm object-cover"
|
||||
className="h-4 w-4 rounded-sm object-cover"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
onError={(event) => {
|
||||
@ -8527,7 +8539,7 @@ export function CanvasChatPanel({
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<TagIcon className="h-3.5 w-3.5 text-current opacity-70" />
|
||||
<TagIcon className="h-4 w-4 text-current opacity-70" />
|
||||
)}
|
||||
<span className="truncate">{tag.name}</span>
|
||||
</button>
|
||||
@ -8535,8 +8547,8 @@ export function CanvasChatPanel({
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
@ -9093,11 +9105,11 @@ export function CanvasChatPanel({
|
||||
)}
|
||||
|
||||
{!isWorkspacePresentation && shouldShowCanvasBanner ? (
|
||||
<div className="px-4 pb-2">
|
||||
<div className="px-[10px] pb-0">
|
||||
<div
|
||||
className={cn(
|
||||
"bg-blue-50 dark:bg-blue-900/20 text-blue-600 dark:text-blue-300 p-3 rounded-xl text-sm flex items-center justify-between",
|
||||
siteSettings?.bannerLink && "cursor-pointer hover:bg-blue-100 dark:hover:bg-blue-900/30 transition-colors"
|
||||
"relative z-10 mb-0 flex h-[48px] items-center justify-between gap-2 rounded-[28px] bg-[#f4f4f5] px-4 py-0 text-[14px] font-medium leading-5 text-[#20242a] dark:bg-zinc-900 dark:text-zinc-100",
|
||||
siteSettings?.bannerLink && "cursor-pointer transition-colors hover:bg-[#eeeeef] dark:hover:bg-zinc-800"
|
||||
)}
|
||||
onClick={() => {
|
||||
if (siteSettings?.bannerLink) {
|
||||
@ -9105,13 +9117,23 @@ export function CanvasChatPanel({
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="p-1 bg-blue-100 dark:bg-blue-800 rounded">
|
||||
<Box className="h-4 w-4" />
|
||||
<div className="flex min-w-0 items-center gap-2.5">
|
||||
<div className="flex h-7 w-7 shrink-0 items-center justify-center rounded-[9px] bg-[#e8ff38] text-[#111827]">
|
||||
<Sparkles className="h-3.5 w-3.5 fill-current" />
|
||||
</div>
|
||||
<span>{siteSettings?.bannerText || "升级会员, Nano Banana Pro 免费365天!"}</span>
|
||||
<span className="min-w-0 truncate">{siteSettings?.bannerText || "限时特惠:升级会员最高立享 45% OFF!"}</span>
|
||||
</div>
|
||||
<X className="h-4 w-4 cursor-pointer opacity-50 hover:opacity-100" onClick={(e) => e.stopPropagation()} />
|
||||
<button
|
||||
type="button"
|
||||
className="flex h-7 w-7 shrink-0 items-center justify-center rounded-full text-[#20242a]/75 transition-colors hover:bg-black/5 hover:text-[#20242a] dark:text-zinc-100/75 dark:hover:bg-white/10 dark:hover:text-zinc-100"
|
||||
aria-label="关闭优惠广告"
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
setIsCanvasBannerDismissed(true);
|
||||
}}
|
||||
>
|
||||
<X className="h-5 w-5" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
@ -9119,14 +9141,16 @@ export function CanvasChatPanel({
|
||||
{/* 底部输入区域 - 混合输入(图片标签+文字可自由混合) */}
|
||||
<div
|
||||
className={cn(
|
||||
"border-t",
|
||||
isWorkspacePresentation
|
||||
? "bg-white/92 p-5 backdrop-blur-sm dark:bg-zinc-950/92"
|
||||
: "bg-white p-4 dark:bg-zinc-950"
|
||||
? "border-t bg-white/92 p-5 backdrop-blur-sm dark:bg-zinc-950/92"
|
||||
: "bg-white px-[10px] pb-[10px] pt-0 dark:bg-zinc-950"
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className="relative flex flex-col rounded-3xl border bg-card transition-all duration-200 focus-within:border-primary/50 focus-within:bg-background dark:border-white/12 dark:bg-zinc-950 dark:focus-within:border-white/25 dark:focus-within:bg-zinc-950"
|
||||
className={cn(
|
||||
"relative flex flex-col overflow-hidden border border-[#e2e5ea] bg-white shadow-[0_22px_42px_-30px_rgba(15,23,42,0.48)] transition-all duration-200 focus-within:border-[#d8dde5] focus-within:shadow-[0_24px_48px_-30px_rgba(15,23,42,0.55)] dark:border-white/12 dark:bg-zinc-950 dark:focus-within:border-white/25",
|
||||
"min-h-[184px] rounded-[28px]"
|
||||
)}
|
||||
data-canvas-chat-input-anchor="true"
|
||||
>
|
||||
{isWorkspacePresentation && !isDetailGenerationMode ? (
|
||||
@ -9883,8 +9907,12 @@ export function CanvasChatPanel({
|
||||
{/* 混合输入区域:使用 contenteditable 实现真正的混合编辑 */}
|
||||
<div
|
||||
className={cn(
|
||||
"relative px-3 pt-3 pb-12",
|
||||
isDetailGenerationMode ? "min-h-[72px]" : "min-h-[60px]"
|
||||
"relative px-3 pb-0 pt-3 sm:px-4",
|
||||
isDetailGenerationMode
|
||||
? "min-h-[112px]"
|
||||
: activeComposerMode === "image"
|
||||
? "min-h-[118px]"
|
||||
: "min-h-[132px]"
|
||||
)}
|
||||
>
|
||||
{!isWorkspacePresentation && !isDetailGenerationMode && selectedSkillTag && (
|
||||
@ -10207,14 +10235,56 @@ export function CanvasChatPanel({
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
<div className="rounded-[22px] border border-[#d7deeb] bg-[#f8fafc] px-4 py-2.5 shadow-[0_12px_24px_-18px_rgba(15,23,42,0.35)] transition-colors focus-within:border-[#a6c8ff] focus-within:bg-white dark:border-zinc-600 dark:bg-zinc-900/60 dark:focus-within:bg-zinc-900">
|
||||
{!isWorkspacePresentation && !isDetailGenerationMode && !isVideoWorkspace && activeComposerMode === "image" ? (
|
||||
<button
|
||||
type="button"
|
||||
className="mx-4 mt-3 flex h-16 w-16 shrink-0 flex-col items-center justify-center gap-1.5 overflow-hidden rounded-[18px] bg-[#f3f3f4] text-[#a8adb5] transition-colors hover:bg-[#ececee] hover:text-[#7d848e] dark:bg-white/8 dark:text-zinc-400 dark:hover:bg-white/12"
|
||||
onClick={() => fileInputRef.current?.click()}
|
||||
disabled={isSubmitting}
|
||||
title="添加参考图"
|
||||
>
|
||||
{attachments[0]?.url ? (
|
||||
<img
|
||||
src={buildPreviewImageUrl(attachments[0].url, 180)}
|
||||
alt={attachments[0].name || "参考图"}
|
||||
className="h-full w-full object-cover"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
onError={(event) => {
|
||||
if (event.currentTarget.src !== attachments[0]?.url) {
|
||||
event.currentTarget.src = attachments[0]?.url || "";
|
||||
}
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<ImagePlus className="h-5 w-5" />
|
||||
<span className="text-[13px] font-medium leading-none">参考图</span>
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
) : null}
|
||||
<div
|
||||
className={cn(
|
||||
isWorkspacePresentation || isDetailGenerationMode || isVideoWorkspace
|
||||
? "px-1"
|
||||
: "px-4 pt-3"
|
||||
)}
|
||||
>
|
||||
<div
|
||||
ref={textareaRef as any}
|
||||
contentEditable={!isSubmitting}
|
||||
suppressContentEditableWarning
|
||||
className={cn(
|
||||
"min-h-[40px] max-h-[120px] overflow-y-auto text-sm leading-6 outline-none empty:before:content-[attr(data-placeholder)]",
|
||||
isVideoWorkspace ? "empty:before:text-muted-foreground/70" : "text-[#111827] empty:before:text-[#9aa1ad] dark:text-zinc-100 dark:empty:before:text-zinc-500"
|
||||
"max-h-[128px] overflow-y-auto outline-none empty:before:content-[attr(data-placeholder)]",
|
||||
isWorkspacePresentation || isDetailGenerationMode || isVideoWorkspace
|
||||
? "min-h-[38px] text-[19px] leading-7 sm:text-[22px] sm:leading-8"
|
||||
: activeComposerMode === "image"
|
||||
? "min-h-[44px] text-[16px] leading-7 sm:text-[16px] sm:leading-7"
|
||||
: "min-h-[64px] text-[16px] leading-7 sm:text-[17px] sm:leading-7",
|
||||
isVideoWorkspace
|
||||
? "text-foreground empty:before:text-muted-foreground/70"
|
||||
: "text-[#1f2328] empty:before:text-[#a8abb2] dark:text-zinc-100 dark:empty:before:text-zinc-500"
|
||||
)}
|
||||
data-placeholder={composerPlaceholder}
|
||||
onInput={(e) => {
|
||||
@ -10281,7 +10351,7 @@ export function CanvasChatPanel({
|
||||
</div>
|
||||
|
||||
{hasAgentUploadSlots && (
|
||||
<div className="mb-11 px-3 pb-1">
|
||||
<div className="mb-3 px-3 pb-1">
|
||||
<div className="flex flex-row gap-2 overflow-x-auto scrollbar-hide">
|
||||
{agentUploadSlots.map((slot, index) => {
|
||||
const slotAttachment = attachments[index];
|
||||
@ -10349,7 +10419,7 @@ export function CanvasChatPanel({
|
||||
)}
|
||||
|
||||
{isVideoModel && videoFrameSlots.length > 0 && (
|
||||
<div className="mb-11 px-3 pb-1">
|
||||
<div className="mb-3 px-3 pb-1">
|
||||
<div className="flex flex-row gap-2">
|
||||
{videoFrameSlots.map((label, index) => {
|
||||
const slotAttachment = attachments[index];
|
||||
@ -10419,10 +10489,10 @@ export function CanvasChatPanel({
|
||||
{/* Bottom Toolbar */}
|
||||
<div
|
||||
className={cn(
|
||||
"absolute inset-x-2 bottom-2 z-10 flex items-center justify-between gap-3 rounded-[24px] border border-[#d7deeb] bg-[#fbfdff]/95 px-3 py-2 shadow-[0_10px_28px_-20px_rgba(15,23,42,0.32)] backdrop-blur-xl supports-[backdrop-filter]:bg-[#fbfdff]/90 dark:border-zinc-700 dark:bg-zinc-900/90 dark:shadow-[0_10px_28px_-20px_rgba(2,6,23,0.7)]"
|
||||
"mt-auto flex min-h-[44px] items-center justify-between gap-1.5 px-3 pb-2 pt-1 dark:bg-zinc-950 sm:px-3 sm:pb-2"
|
||||
)}
|
||||
>
|
||||
<div className="flex min-w-0 flex-1 items-center gap-2">
|
||||
<div className="flex min-w-0 flex-1 items-center gap-1 overflow-x-auto pr-1 scrollbar-hide">
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
@ -10459,56 +10529,75 @@ export function CanvasChatPanel({
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
{!isWorkspacePresentation && !isDetailGenerationMode && activeComposerMode !== "image" && (!isVideoModel || showVideoAttachmentButton) && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className={cn(
|
||||
"h-8 w-8 shrink-0 rounded-full border border-transparent text-[#20242a] transition-colors hover:bg-[#f3f4f6] hover:text-[#111418] dark:hover:border-zinc-500 dark:hover:bg-zinc-700/70",
|
||||
isVideoWorkspace
|
||||
? "bg-background"
|
||||
: "dark:bg-white/8 dark:text-zinc-200 dark:hover:bg-white/12 dark:hover:text-white"
|
||||
)}
|
||||
onClick={() => fileInputRef.current?.click()}
|
||||
disabled={isSubmitting}
|
||||
title={getToolbarItem("chatAttach").label}
|
||||
>
|
||||
<CanvasToolbarIcon
|
||||
className="h-4.5 w-4.5"
|
||||
fallback={Plus}
|
||||
item={getToolbarItem("chatAttach")}
|
||||
/>
|
||||
</Button>
|
||||
)}
|
||||
{!isWorkspacePresentation ? (
|
||||
<DropdownMenu>
|
||||
<DropdownMenu
|
||||
open={isComposerModeMenuOpen}
|
||||
onOpenChange={setIsComposerModeMenuOpen}
|
||||
>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
className={cn(
|
||||
"relative h-8 cursor-pointer rounded-full px-3 text-sm font-medium",
|
||||
isVideoWorkspace
|
||||
? "border border-[#cfd8e6] bg-[#f3f7ff] text-[#2f4f9e] shadow-[0_2px_8px_-4px_rgba(47,79,158,0.35)] hover:bg-[#e8f0ff] dark:border-zinc-500/60 dark:bg-zinc-800 dark:text-zinc-100 dark:hover:bg-zinc-700"
|
||||
: activeComposerMode === "video"
|
||||
? "border border-blue-500 bg-blue-50 text-blue-600 hover:bg-blue-100 dark:border-blue-400/40 dark:bg-blue-500/15 dark:text-blue-200 dark:hover:bg-blue-500/20"
|
||||
: activeComposerMode === "detail"
|
||||
? "border border-violet-500 bg-violet-50 text-violet-700 hover:bg-violet-100 dark:border-violet-400/40 dark:bg-violet-500/15 dark:text-violet-200 dark:hover:bg-violet-500/20"
|
||||
: activeComposerMode === "image"
|
||||
? "border border-purple-500 bg-purple-50 text-purple-700 hover:bg-purple-100 dark:border-purple-400/40 dark:bg-purple-500/15 dark:text-purple-200 dark:hover:bg-purple-500/20"
|
||||
: "border border-[#9eb3ff] bg-white text-zinc-700 hover:bg-zinc-50 dark:border-white/10 dark:bg-white/8 dark:text-zinc-200 dark:hover:bg-white/12"
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
className={cn(
|
||||
"relative h-9 max-w-[122px] shrink-0 cursor-pointer rounded-full px-2.5 text-[15px] font-medium outline-none focus:outline-none focus:ring-0 focus:ring-offset-0 focus-visible:outline-none focus-visible:ring-0 focus-visible:ring-offset-0",
|
||||
isVideoWorkspace
|
||||
? "bg-muted/80 text-foreground hover:bg-muted dark:bg-zinc-800 dark:text-zinc-100 dark:hover:bg-zinc-700"
|
||||
: "bg-[#eeeeef] text-[#20242a] hover:bg-[#e7e7e8] dark:bg-white/10 dark:text-zinc-100 dark:hover:bg-white/14"
|
||||
)}
|
||||
>
|
||||
<span className="flex items-center">
|
||||
<span className="flex min-w-0 items-center">
|
||||
{activeComposerMode === "video" ? (
|
||||
<CanvasToolbarIcon
|
||||
className="mr-1 h-4 w-4"
|
||||
className="mr-1.5 h-5 w-5 shrink-0"
|
||||
fallback={Video}
|
||||
item={getToolbarItem("chatModeVideoActive")}
|
||||
/>
|
||||
) : activeComposerMode === "detail" ? (
|
||||
<CanvasToolbarIcon
|
||||
className="mr-1 h-4 w-4"
|
||||
className="mr-1.5 h-5 w-5 shrink-0"
|
||||
fallback={LayoutGrid}
|
||||
item={getToolbarItem("chatModeDetailActive")}
|
||||
/>
|
||||
) : activeComposerMode === "image" ? (
|
||||
<CanvasToolbarIcon
|
||||
className="mr-1 h-4 w-4"
|
||||
className="mr-1.5 h-5 w-5 shrink-0"
|
||||
fallback={ImagePlus}
|
||||
item={getToolbarItem("chatModeImageActive")}
|
||||
/>
|
||||
) : (
|
||||
<CanvasToolbarIcon
|
||||
className="mr-1 h-4 w-4"
|
||||
fallback={Sparkles}
|
||||
item={getToolbarItem(
|
||||
isPresetAgentSelection
|
||||
? "chatModePresetAgent"
|
||||
: "chatModeAgentActive"
|
||||
)}
|
||||
className="mr-1.5 h-5 w-5 shrink-0"
|
||||
fallback={Bot}
|
||||
item={null}
|
||||
/>
|
||||
)}
|
||||
{activeModeLabel}
|
||||
<span className="min-w-0 truncate">{activeModeLabel}</span>
|
||||
{isComposerModeMenuOpen ? (
|
||||
<ChevronUp className="ml-1.5 h-3.5 w-3.5 shrink-0" />
|
||||
) : (
|
||||
<ChevronDown className="ml-1.5 h-3.5 w-3.5 shrink-0" />
|
||||
)}
|
||||
</span>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
@ -10517,61 +10606,61 @@ export function CanvasChatPanel({
|
||||
side="top"
|
||||
sideOffset={10}
|
||||
className={cn(
|
||||
"w-[156px] rounded-xl p-1.5",
|
||||
"w-[200px] rounded-[18px] border border-[#d8dce3] bg-white p-2 text-[#20242a] shadow-[0_22px_54px_rgba(15,23,42,0.16)]",
|
||||
isVideoWorkspace
|
||||
? "border border-border/60 bg-background/95 shadow-[0_18px_36px_-24px_rgba(15,23,42,0.35)] backdrop-blur-xl"
|
||||
: "border border-[#d6d9df] bg-[#f4f5f7]/90 shadow-[0_8px_18px_rgba(15,23,42,0.14)] dark:border-white/10 dark:bg-zinc-900/95"
|
||||
? "border border-border/60 bg-background/95 shadow-[0_14px_30px_-20px_rgba(15,23,42,0.35)] backdrop-blur-xl"
|
||||
: "dark:border-white/10 dark:bg-zinc-900/95 dark:text-zinc-100"
|
||||
)}
|
||||
>
|
||||
{effectiveAgentEnabled ? (
|
||||
<DropdownMenuItem
|
||||
onClick={() => handleSelectGenerationMode("agent")}
|
||||
className={cn(
|
||||
"h-8 cursor-pointer rounded-lg px-2 text-[13px] font-medium",
|
||||
"h-11 cursor-pointer rounded-[12px] px-3 text-[16px] font-normal [&_svg]:!size-5",
|
||||
isVideoWorkspace
|
||||
? "text-foreground"
|
||||
: "text-[#2e3238] dark:text-zinc-100",
|
||||
activeComposerMode === "agent"
|
||||
? isVideoWorkspace
|
||||
? "bg-background"
|
||||
: "bg-white/80 dark:bg-white/10"
|
||||
: "bg-transparent dark:bg-white/10"
|
||||
: isVideoWorkspace
|
||||
? "hover:bg-background/80"
|
||||
: "hover:bg-white/70 dark:hover:bg-white/8"
|
||||
: "hover:bg-[#f5f6f7] dark:hover:bg-white/8"
|
||||
)}
|
||||
>
|
||||
<CanvasToolbarIcon
|
||||
className="h-4 w-4"
|
||||
fallback={Sparkles}
|
||||
item={getToolbarItem("chatModeAgentMenu")}
|
||||
/>
|
||||
<span className="flex-1">
|
||||
{getToolbarItem("chatModeAgentMenu").label}
|
||||
</span>
|
||||
{activeComposerMode === "agent" ? (
|
||||
<Check className={cn("h-4 w-4", isVideoWorkspace ? "text-primary" : "text-[#3b82f6]")} />
|
||||
) : null}
|
||||
</DropdownMenuItem>
|
||||
>
|
||||
<CanvasToolbarIcon
|
||||
className="h-5 w-5"
|
||||
fallback={Bot}
|
||||
item={null}
|
||||
/>
|
||||
<span className="flex-1">
|
||||
Agent
|
||||
</span>
|
||||
{activeComposerMode === "agent" ? (
|
||||
<Check className={cn("h-4 w-4", isVideoWorkspace ? "text-primary" : "text-[#1f2937]")} />
|
||||
) : null}
|
||||
</DropdownMenuItem>
|
||||
) : null}
|
||||
{canUseDetailGeneration ? (
|
||||
<DropdownMenuItem
|
||||
onClick={() => handleSelectGenerationMode("detail")}
|
||||
className={cn(
|
||||
"h-8 cursor-pointer rounded-lg px-2 text-[13px] font-medium",
|
||||
"h-11 cursor-pointer rounded-[12px] px-3 text-[16px] font-normal [&_svg]:!size-5",
|
||||
isVideoWorkspace
|
||||
? "text-foreground"
|
||||
: "text-[#2e3238] dark:text-zinc-100",
|
||||
activeComposerMode === "detail"
|
||||
? isVideoWorkspace
|
||||
? "bg-background"
|
||||
: "bg-white/80 dark:bg-white/10"
|
||||
: "bg-transparent dark:bg-white/10"
|
||||
: isVideoWorkspace
|
||||
? "hover:bg-background/80"
|
||||
: "hover:bg-white/70 dark:hover:bg-white/8"
|
||||
: "hover:bg-[#f5f6f7] dark:hover:bg-white/8"
|
||||
)}
|
||||
>
|
||||
<CanvasToolbarIcon
|
||||
className="h-4 w-4"
|
||||
className="h-5 w-5"
|
||||
fallback={LayoutGrid}
|
||||
item={getToolbarItem("chatModeDetailMenu")}
|
||||
/>
|
||||
@ -10579,92 +10668,70 @@ export function CanvasChatPanel({
|
||||
{getToolbarItem("chatModeDetailMenu").label}
|
||||
</span>
|
||||
{activeComposerMode === "detail" ? (
|
||||
<Check className={cn("h-4 w-4", isVideoWorkspace ? "text-primary" : "text-[#3b82f6]")} />
|
||||
<Check className={cn("h-4 w-4", isVideoWorkspace ? "text-primary" : "text-[#1f2937]")} />
|
||||
) : null}
|
||||
</DropdownMenuItem>
|
||||
) : null}
|
||||
<DropdownMenuItem
|
||||
onClick={() => handleSelectGenerationMode("image")}
|
||||
className={cn(
|
||||
"h-8 cursor-pointer rounded-lg px-2 text-[13px] font-medium",
|
||||
"h-11 cursor-pointer rounded-[12px] px-3 text-[16px] font-normal [&_svg]:!size-5",
|
||||
isVideoWorkspace
|
||||
? "text-foreground"
|
||||
: "text-[#2e3238] dark:text-zinc-100",
|
||||
activeComposerMode === "image"
|
||||
? isVideoWorkspace
|
||||
? "bg-background"
|
||||
: "bg-white/80 dark:bg-white/10"
|
||||
: "bg-transparent dark:bg-white/10"
|
||||
: isVideoWorkspace
|
||||
? "hover:bg-background/80"
|
||||
: "hover:bg-white/70 dark:hover:bg-white/8"
|
||||
: "hover:bg-[#f5f6f7] dark:hover:bg-white/8"
|
||||
)}
|
||||
>
|
||||
<CanvasToolbarIcon
|
||||
className="h-4 w-4"
|
||||
className="h-5 w-5"
|
||||
fallback={ImagePlus}
|
||||
item={getToolbarItem("chatModeImageMenu")}
|
||||
/>
|
||||
<span className="flex-1">
|
||||
{getToolbarItem("chatModeImageMenu").label}
|
||||
图像
|
||||
</span>
|
||||
{activeComposerMode === "image" ? (
|
||||
<Check className={cn("h-4 w-4", isVideoWorkspace ? "text-primary" : "text-[#3b82f6]")} />
|
||||
<Check className={cn("h-4 w-4", isVideoWorkspace ? "text-primary" : "text-[#1f2937]")} />
|
||||
) : null}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={() => handleSelectGenerationMode("video")}
|
||||
className={cn(
|
||||
"h-8 cursor-pointer rounded-lg px-2 text-[13px] font-medium",
|
||||
"h-11 cursor-pointer rounded-[12px] px-3 text-[16px] font-normal [&_svg]:!size-5",
|
||||
isVideoWorkspace
|
||||
? "text-foreground"
|
||||
: "text-[#2e3238] dark:text-zinc-100",
|
||||
activeComposerMode === "video"
|
||||
? isVideoWorkspace
|
||||
? "bg-background"
|
||||
: "bg-white/80 dark:bg-white/10"
|
||||
: "bg-transparent dark:bg-white/10"
|
||||
: isVideoWorkspace
|
||||
? "hover:bg-background/80"
|
||||
: "hover:bg-white/70 dark:hover:bg-white/8"
|
||||
: "hover:bg-[#f5f6f7] dark:hover:bg-white/8"
|
||||
)}
|
||||
>
|
||||
<CanvasToolbarIcon
|
||||
className="h-4 w-4"
|
||||
className="h-5 w-5"
|
||||
fallback={Video}
|
||||
item={getToolbarItem("chatModeVideoMenu")}
|
||||
/>
|
||||
<span className="flex-1">
|
||||
{getToolbarItem("chatModeVideoMenu").label}
|
||||
视频
|
||||
</span>
|
||||
{activeComposerMode === "video" ? (
|
||||
<Check className={cn("h-4 w-4", isVideoWorkspace ? "text-primary" : "text-[#3b82f6]")} />
|
||||
<Check className={cn("h-4 w-4", isVideoWorkspace ? "text-primary" : "text-[#1f2937]")} />
|
||||
) : null}
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
) : null}
|
||||
|
||||
{!isWorkspacePresentation && !isDetailGenerationMode && (!isVideoModel || showVideoAttachmentButton) && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className={cn(
|
||||
"h-8 w-8 rounded-full text-muted-foreground transition-colors hover:text-foreground mr-1 border border-transparent hover:border-[#cfd8e6] hover:bg-[#eef4ff] dark:hover:border-zinc-500 dark:hover:bg-zinc-700/70",
|
||||
isVideoWorkspace
|
||||
? "bg-background"
|
||||
: "hover:bg-muted dark:bg-white/8 dark:text-zinc-200 dark:hover:bg-white/12 dark:hover:text-white"
|
||||
)}
|
||||
onClick={() => fileInputRef.current?.click()}
|
||||
disabled={isSubmitting}
|
||||
title={getToolbarItem("chatAttach").label}
|
||||
>
|
||||
<CanvasToolbarIcon
|
||||
className="h-4 w-4"
|
||||
fallback={Paperclip}
|
||||
item={getToolbarItem("chatAttach")}
|
||||
/>
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{isVideoModel ? (
|
||||
<div className="min-w-0 flex-1 overflow-x-auto scrollbar-hide">
|
||||
<div className="w-max pr-1 pl-1">
|
||||
@ -10678,49 +10745,24 @@ export function CanvasChatPanel({
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="min-w-0 shrink pl-2 border-l border-[#d9e1f3]/70 dark:border-white/10 ml-1">
|
||||
<ImageParamsBar
|
||||
modelName={activeComposerMode === "agent" ? agentDisplayName : currentModel?.displayName}
|
||||
initialParams={imageParams}
|
||||
onParamsChange={handleImageParamsChange}
|
||||
inline={true}
|
||||
providerType={currentModel?.providerType}
|
||||
imageGenConfig={currentModel?.imageGenConfig || undefined}
|
||||
hideImageCount={activeComposerMode === "agent"}
|
||||
hideAdvancedOptions={activeComposerMode === "image"}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<div className="flex shrink-0 items-center gap-2 pl-2 border-l border-[#d9e1f3]/70 dark:border-white/10">
|
||||
{!isWorkspacePresentation && activeComposerMode === "image" ? (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
title={`${getToolbarItem("chatDeepThinking").label}:${deepThinkingEnabled ? "开" : "关"}`}
|
||||
className={cn(
|
||||
"h-8 w-8 shrink-0 rounded-full border-0 bg-transparent p-0 shadow-none transition-colors",
|
||||
isVideoWorkspace
|
||||
? deepThinkingEnabled
|
||||
? "bg-primary/10 text-primary hover:bg-primary/12"
|
||||
: "text-muted-foreground hover:bg-muted/70"
|
||||
: deepThinkingEnabled
|
||||
? "bg-[#eef4ff] text-[#3b5bcc] hover:bg-[#e5efff] dark:bg-blue-500/15 dark:text-blue-200 dark:hover:bg-blue-500/20"
|
||||
: "text-[#6b7280] hover:bg-[#f4f6f8] dark:bg-white/8 dark:text-zinc-200 dark:hover:bg-white/12 dark:hover:text-white"
|
||||
)}
|
||||
onClick={() => setDeepThinkingEnabled((prev) => !prev)}
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
<CanvasToolbarIcon
|
||||
className="h-4 w-4"
|
||||
fallback={Brain}
|
||||
item={getToolbarItem("chatDeepThinking")}
|
||||
/>
|
||||
</Button>
|
||||
<div className="flex shrink-0 items-center gap-1">
|
||||
{!isVideoModel && !isWorkspacePresentation && activeComposerMode === "image" ? (
|
||||
<ImageParamsBar
|
||||
modelName={currentModel?.displayName}
|
||||
initialParams={imageParams}
|
||||
onParamsChange={handleImageParamsChange}
|
||||
inline={true}
|
||||
providerType={currentModel?.providerType}
|
||||
imageGenConfig={currentModel?.imageGenConfig || undefined}
|
||||
hideImageCount={false}
|
||||
hideAdvancedOptions={true}
|
||||
inlineTheme="minimal"
|
||||
showInlineDivider={false}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{!isWorkspacePresentation && activeComposerMode !== "detail" ? (
|
||||
<Select
|
||||
value={modelSelectValue}
|
||||
@ -10748,22 +10790,22 @@ export function CanvasChatPanel({
|
||||
>
|
||||
<SelectTrigger
|
||||
className={cn(
|
||||
"h-8 w-8 rounded-full p-0 justify-center [&>svg]:hidden",
|
||||
"h-8 w-8 shrink-0 justify-center rounded-full p-0 outline-none focus:outline-none focus:ring-0 focus:ring-offset-0 focus-visible:outline-none focus-visible:ring-0 focus-visible:ring-offset-0 [&>svg]:hidden",
|
||||
isVideoWorkspace
|
||||
? "border border-[#cfd8e6] bg-white/85 text-zinc-700 hover:bg-white dark:border-white/20 dark:bg-white dark:text-zinc-950 dark:hover:bg-zinc-100"
|
||||
: "border border-[#cfd4dc] bg-white dark:border-white/20 dark:bg-white dark:text-zinc-950 dark:hover:bg-zinc-100"
|
||||
? "border border-[#cfd8e6] bg-white/85 text-zinc-700 hover:bg-white dark:border-white/12 dark:bg-[#242529] dark:text-zinc-100 dark:hover:bg-[#2c2d33]"
|
||||
: "border border-[#cfd4dc] bg-white text-[#20242a] dark:border-white/12 dark:bg-[#242529] dark:text-zinc-100 dark:hover:bg-[#2c2d33]"
|
||||
)}
|
||||
title={getToolbarItem("chatModelSelector").label}
|
||||
>
|
||||
<span
|
||||
className={cn(
|
||||
"flex h-6 w-6 items-center justify-center overflow-hidden rounded-full",
|
||||
isVideoWorkspace ? "bg-background" : "bg-white dark:bg-white"
|
||||
"flex h-4 w-4 items-center justify-center overflow-hidden rounded-full",
|
||||
isVideoWorkspace ? "bg-background dark:bg-transparent" : "bg-white dark:bg-transparent"
|
||||
)}
|
||||
>
|
||||
{getToolbarItem("chatModelSelector").iconUrl ? (
|
||||
<CanvasToolbarIcon
|
||||
className="h-4 w-4"
|
||||
className="h-3 w-3"
|
||||
fallback={Bot}
|
||||
item={getToolbarItem("chatModelSelector")}
|
||||
/>
|
||||
@ -10783,8 +10825,8 @@ export function CanvasChatPanel({
|
||||
) : (
|
||||
<span
|
||||
className={cn(
|
||||
"flex h-full w-full items-center justify-center text-[10px] font-medium",
|
||||
isVideoWorkspace ? "text-foreground/70" : "text-[#4b5563] dark:text-zinc-950"
|
||||
"flex h-full w-full items-center justify-center text-[9px] font-medium",
|
||||
isVideoWorkspace ? "text-foreground/70 dark:text-zinc-100" : "text-[#4b5563] dark:text-zinc-100"
|
||||
)}
|
||||
>
|
||||
{(selectDisplayModel?.displayName || "A").slice(0, 1)}
|
||||
@ -10799,7 +10841,7 @@ export function CanvasChatPanel({
|
||||
{estimatedPointsCost ? (
|
||||
<div
|
||||
className={cn(
|
||||
"flex h-8 items-center gap-1.5 rounded-full px-2.5 text-xs ml-2",
|
||||
"ml-0 flex h-7 max-w-[60px] shrink items-center gap-1 rounded-full px-2 text-[10px] sm:ml-1 sm:max-w-none sm:gap-1 sm:px-2 sm:text-[11px]",
|
||||
isVideoWorkspace
|
||||
? "border border-border/60 bg-background text-muted-foreground"
|
||||
: "bg-muted text-muted-foreground"
|
||||
@ -10807,14 +10849,14 @@ export function CanvasChatPanel({
|
||||
title={estimatedPointsCost.title}
|
||||
>
|
||||
<CanvasToolbarIcon
|
||||
className="h-3.5 w-3.5"
|
||||
className="h-3 w-3"
|
||||
fallback={Sparkles}
|
||||
item={getToolbarItem("chatPoints")}
|
||||
/>
|
||||
<span className="font-medium text-foreground">
|
||||
{estimatedPointsCost.cost}
|
||||
</span>
|
||||
<span>{getToolbarItem("chatPoints").label}</span>
|
||||
<span className="hidden sm:inline">{getToolbarItem("chatPoints").label}</span>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
@ -10833,8 +10875,8 @@ 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-[#2f80ff] text-white shadow-sm shadow-[#2f80ff]/30 hover:bg-[#2570e8]"
|
||||
: "bg-muted text-muted-foreground hover:bg-muted/80"
|
||||
? "bg-[#1f2328] text-white shadow-sm hover:bg-[#111418]"
|
||||
: "bg-[#f1f1f2] text-[#b6bac2] hover:bg-[#ececee] dark:bg-[#242529] dark:text-[#777d88] dark:hover:bg-[#2c2d33]"
|
||||
)}
|
||||
onClick={handleSend}
|
||||
disabled={isSubmitting || !canSend}
|
||||
@ -10853,7 +10895,7 @@ export function CanvasChatPanel({
|
||||
<>
|
||||
<CanvasToolbarIcon
|
||||
className={cn(
|
||||
"h-4 w-4",
|
||||
"h-3.5 w-3.5",
|
||||
canSend &&
|
||||
(isVideoWorkspace || isDetailGenerationMode
|
||||
? "opacity-95"
|
||||
@ -10873,7 +10915,7 @@ export function CanvasChatPanel({
|
||||
<>
|
||||
<CanvasToolbarIcon
|
||||
className={cn(
|
||||
"h-4 w-4",
|
||||
"h-3.5 w-3.5",
|
||||
canSend &&
|
||||
(isVideoWorkspace || isDetailGenerationMode
|
||||
? "opacity-95"
|
||||
|
||||
@ -1845,6 +1845,11 @@ export function CanvasWorkspace({
|
||||
tool === 'draw' && "cursor-crosshair",
|
||||
isPanning && "cursor-grabbing"
|
||||
)}
|
||||
style={
|
||||
isWorkflowCanvas
|
||||
? undefined
|
||||
: { backgroundColor: "var(--canvas-surface-background, #f4f4f5)" }
|
||||
}
|
||||
onClick={handleCanvasClick}
|
||||
onMouseDown={handleMouseDown}
|
||||
onMouseMove={handleMouseMove}
|
||||
|
||||
@ -48,7 +48,6 @@ import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { HoverCard, HoverCardContent, HoverCardTrigger } from "@/components/ui/hover-card";
|
||||
@ -323,7 +322,7 @@ type LovartPromptTagKey = (typeof PROMPT_TAGS)[number]["key"];
|
||||
type LovartPromptTagVisibility = Record<LovartPromptTagKey, boolean>;
|
||||
|
||||
const FEATURED_PAGE_SIZE = 24;
|
||||
const COMPOSER_PREFIX_DEFAULT = "让Buyi-ai";
|
||||
const COMPOSER_PREFIX_DEFAULT = "让「由壹点AI」";
|
||||
|
||||
function SidebarSvgIcon({
|
||||
iconUrl,
|
||||
@ -425,14 +424,6 @@ export function LovartHome({
|
||||
}: LovartHomeProps = {}) {
|
||||
const router = useRouter();
|
||||
const { theme, resolvedTheme, setTheme } = useTheme();
|
||||
const sidebarPopupBaseClass =
|
||||
"rounded-[24px] border border-black/8 bg-white/96 p-2.5 text-[#111827] shadow-[0_24px_64px_rgba(15,23,42,0.12)] backdrop-blur-xl";
|
||||
const lovartSidebarButtonClass = (active = false) =>
|
||||
cn(
|
||||
"group flex h-10 w-10 items-center justify-center rounded-xl text-[#6b7280] transition-all duration-200 hover:bg-black/[0.04] hover:text-[#111827]",
|
||||
active &&
|
||||
"bg-[#111827] text-white shadow-[0_10px_22px_rgba(15,23,42,0.18)]"
|
||||
);
|
||||
const [prompt, setPrompt] = useState("");
|
||||
const [files, setFiles] = useState<UploadFileItem[]>([]);
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
@ -452,6 +443,7 @@ export function LovartHome({
|
||||
imageSize: "2K",
|
||||
imageCount: 1,
|
||||
});
|
||||
const [composerModelId, setComposerModelId] = useState("auto");
|
||||
const [loginDialogOpen, setLoginDialogOpen] = useState(false);
|
||||
const [loginEmail, setLoginEmail] = useState("");
|
||||
const [loginPassword, setLoginPassword] = useState("");
|
||||
@ -697,8 +689,19 @@ export function LovartHome({
|
||||
[modelsData?.agentModel?.avatarUrl, modelsData?.agentModel?.imageGenConfig]
|
||||
);
|
||||
|
||||
const composerModelValue = "auto";
|
||||
const triggerComposerModel = autoComposerModel;
|
||||
const composerModelOptions = useMemo<ModelOption[]>(() => {
|
||||
const seen = new Set<string>(["auto"]);
|
||||
const imageModels = (modelsData?.models || []).filter((model) => {
|
||||
if (!model?.id || seen.has(model.id)) return false;
|
||||
if (model.supportsImageGen === false) return false;
|
||||
seen.add(model.id);
|
||||
return true;
|
||||
});
|
||||
return [autoComposerModel, ...imageModels];
|
||||
}, [autoComposerModel, modelsData?.models]);
|
||||
const selectedComposerModel =
|
||||
composerModelOptions.find((model) => model.id === composerModelId) ||
|
||||
autoComposerModel;
|
||||
const isMembershipEnabled = siteSettingsData?.membershipConfig?.enabled !== false;
|
||||
|
||||
const discoveryTabs = useMemo(() => {
|
||||
@ -938,15 +941,12 @@ export function LovartHome({
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleImageParamsChange = (nextParams: ImageGenerationParams) => {
|
||||
setImageParams(nextParams);
|
||||
if (typeof window === "undefined") return;
|
||||
try {
|
||||
localStorage.setItem("lovart-home-image-params", JSON.stringify(nextParams));
|
||||
} catch (error) {
|
||||
console.error("[LovartHome] Failed to save image params:", error);
|
||||
useEffect(() => {
|
||||
if (composerModelOptions.some((model) => model.id === composerModelId)) {
|
||||
return;
|
||||
}
|
||||
};
|
||||
setComposerModelId("auto");
|
||||
}, [composerModelId, composerModelOptions]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!featuredLoadMoreRef.current || !hasMoreFeatured) return;
|
||||
@ -1340,7 +1340,7 @@ export function LovartHome({
|
||||
attachments: uploadedAttachments,
|
||||
autoSend: true,
|
||||
createdAt: Date.now(),
|
||||
selectedModelId: "auto",
|
||||
selectedModelId: selectedComposerModel.id || "auto",
|
||||
imageParams,
|
||||
})
|
||||
);
|
||||
@ -1461,11 +1461,15 @@ export function LovartHome({
|
||||
imgClassName={cn(
|
||||
sizeClassName,
|
||||
"object-contain",
|
||||
options?.active ? "brightness-0 invert opacity-100" : "brightness-0 opacity-60"
|
||||
options?.active
|
||||
? "brightness-0 opacity-85 dark:invert dark:opacity-95"
|
||||
: "brightness-0 opacity-60 dark:invert dark:opacity-70"
|
||||
)}
|
||||
fallbackClassName={cn(
|
||||
sizeClassName,
|
||||
options?.active ? "text-white" : "text-[#6b7280]"
|
||||
options?.active
|
||||
? "text-[#111827] dark:text-white"
|
||||
: "text-[#6b7280] dark:text-white/62"
|
||||
)}
|
||||
/>
|
||||
);
|
||||
@ -1590,66 +1594,41 @@ export function LovartHome({
|
||||
<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"
|
||||
className="inline-flex h-8 w-8 items-center justify-center rounded-[11px] border border-black/10 bg-white 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" />
|
||||
<SelectedThemeIcon className="h-[15px] w-[15px]" />
|
||||
</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]"
|
||||
className="!min-w-0 w-auto rounded-[18px] 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;
|
||||
<div className="flex items-center gap-1">
|
||||
{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
|
||||
return (
|
||||
<DropdownMenuItem
|
||||
key={option.value}
|
||||
aria-label={option.label}
|
||||
className={cn(
|
||||
"flex h-8 w-8 items-center justify-center rounded-xl",
|
||||
"flex h-[34px] w-[34px] cursor-pointer items-center justify-center rounded-[10px] p-0 focus:bg-black/5 dark:focus:bg-white/8",
|
||||
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"
|
||||
? "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/70 dark:text-white/70"
|
||||
)}
|
||||
onSelect={() => setTheme(option.value)}
|
||||
title={option.label}
|
||||
>
|
||||
<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>
|
||||
);
|
||||
})}
|
||||
<OptionIcon className="h-[15px] w-[15px]" />
|
||||
</DropdownMenuItem>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
|
||||
@ -1779,7 +1758,6 @@ export function LovartHome({
|
||||
onImageEditor={handleOpenImageEditor}
|
||||
onNavLink={openNavLink}
|
||||
onNewCanvas={openNewCanvas}
|
||||
onProfile={() => toast.info("个人主页模块后续开放")}
|
||||
onRoute={openNavLink}
|
||||
onTutorial={handleOpenTutorial}
|
||||
productPlanningEnabled={siteSettingsData?.productPlanningEnabled !== false}
|
||||
@ -1798,6 +1776,7 @@ export function LovartHome({
|
||||
tutorialEnabled={Boolean(siteSettingsData?.tutorialEnabled)}
|
||||
tutorialVideos={tutorialVideos}
|
||||
videoCanvasEnabled={siteSettingsData?.videoCanvasEnabled !== false}
|
||||
tone={resolvedTheme === "dark" ? "dark" : "light"}
|
||||
/>
|
||||
|
||||
<main className="mx-auto max-w-[1240px] px-6 pb-20 pt-[84px]">
|
||||
@ -1807,15 +1786,16 @@ export function LovartHome({
|
||||
displayLogo={displayLogo}
|
||||
files={files}
|
||||
handleAddFiles={handleAddFiles}
|
||||
handleImageParamsChange={handleImageParamsChange}
|
||||
heroPromoActionText={heroPromoActionText}
|
||||
heroPromoText={heroPromoText}
|
||||
heroSubtitleText={heroSubtitleText}
|
||||
heroTitleText={heroTitleText}
|
||||
homeAnimationData={homeAnimationData}
|
||||
imageParams={imageParams}
|
||||
isMembershipEnabled={isMembershipEnabled}
|
||||
isSubmitting={isSubmitting}
|
||||
modelOptions={composerModelOptions}
|
||||
onModelChange={setComposerModelId}
|
||||
onStartAgentMode={() => setComposerModelId("auto")}
|
||||
onSubmit={createCanvasFromComposer}
|
||||
openUpgradeDialog={() => setUpgradeDialogOpen(true)}
|
||||
prompt={prompt}
|
||||
@ -1823,7 +1803,7 @@ export function LovartHome({
|
||||
setPrompt={setPrompt}
|
||||
shouldInvertLogo={shouldInvertLogo}
|
||||
siteName={siteName}
|
||||
triggerComposerModel={triggerComposerModel}
|
||||
selectedModelId={selectedComposerModel.id}
|
||||
visiblePromptTags={visiblePromptTags}
|
||||
/>
|
||||
)}
|
||||
|
||||
@ -1,8 +1,13 @@
|
||||
import { ArrowUp, Loader2, Sparkles, Upload } from "lucide-react";
|
||||
import { ArrowUp, Box, Lightbulb, Loader2, Plus, Sparkles } from "lucide-react";
|
||||
import type { ChangeEvent } from "react";
|
||||
import { HomeAnimationEmbed } from "@/components/home/home-animation-embed";
|
||||
import { ImageParamsBar, type ImageGenerationParams } from "@/components/image-params-bar";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
} from "@/components/ui/select";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import type { ModelOption, UploadFileItem } from "./types";
|
||||
import type { PROMPT_TAGS } from "./utils";
|
||||
@ -14,7 +19,6 @@ type LovartHeroComposerProps = {
|
||||
displayLogo: string;
|
||||
files: UploadFileItem[];
|
||||
handleAddFiles: (files: FileList | null) => void;
|
||||
handleImageParamsChange: (nextParams: ImageGenerationParams) => void;
|
||||
heroPromoActionText?: string | null;
|
||||
heroPromoText?: string | null;
|
||||
heroSubtitleText?: string | null;
|
||||
@ -24,7 +28,6 @@ type LovartHeroComposerProps = {
|
||||
htmlContent?: string;
|
||||
fileName?: string;
|
||||
};
|
||||
imageParams: ImageGenerationParams;
|
||||
isMembershipEnabled: boolean;
|
||||
isSubmitting: boolean;
|
||||
onSubmit: () => void;
|
||||
@ -34,7 +37,10 @@ type LovartHeroComposerProps = {
|
||||
setPrompt: (value: string) => void;
|
||||
shouldInvertLogo: boolean;
|
||||
siteName: string;
|
||||
triggerComposerModel: ModelOption;
|
||||
modelOptions: ModelOption[];
|
||||
onModelChange: (modelId: string) => void;
|
||||
onStartAgentMode: () => void;
|
||||
selectedModelId: string;
|
||||
visiblePromptTags: PromptTag[];
|
||||
};
|
||||
|
||||
@ -43,13 +49,11 @@ export function LovartHeroComposer({
|
||||
displayLogo,
|
||||
files,
|
||||
handleAddFiles,
|
||||
handleImageParamsChange,
|
||||
heroPromoActionText,
|
||||
heroPromoText,
|
||||
heroSubtitleText,
|
||||
heroTitleText,
|
||||
homeAnimationData,
|
||||
imageParams,
|
||||
isMembershipEnabled,
|
||||
isSubmitting,
|
||||
onSubmit,
|
||||
@ -59,7 +63,10 @@ export function LovartHeroComposer({
|
||||
setPrompt,
|
||||
shouldInvertLogo,
|
||||
siteName,
|
||||
triggerComposerModel,
|
||||
modelOptions,
|
||||
onModelChange,
|
||||
onStartAgentMode,
|
||||
selectedModelId,
|
||||
visiblePromptTags,
|
||||
}: LovartHeroComposerProps) {
|
||||
return (
|
||||
@ -127,9 +134,9 @@ export function LovartHeroComposer({
|
||||
)}
|
||||
|
||||
<div className="flex items-center justify-between border-t border-black/8 pt-3 dark:border-white/8">
|
||||
<div className="flex min-w-0 flex-1 items-center gap-1.5">
|
||||
<label className="inline-flex h-9 w-9 cursor-pointer items-center justify-center rounded-full border border-black/10 text-black/60 hover:bg-black/5 dark:border-white/12 dark:text-white/60 dark:hover:bg-white/8">
|
||||
<Upload className="h-4 w-4" />
|
||||
<div className="flex min-w-0 flex-1 items-center gap-4">
|
||||
<label className="inline-flex h-9 w-9 cursor-pointer items-center justify-center rounded-full text-[#20242a] hover:bg-black/5 dark:text-white/72 dark:hover:bg-white/8">
|
||||
<Plus className="h-5 w-5" />
|
||||
<input
|
||||
type="file"
|
||||
accept="image/*"
|
||||
@ -141,49 +148,66 @@ export function LovartHeroComposer({
|
||||
}}
|
||||
/>
|
||||
</label>
|
||||
|
||||
<div className="inline-flex h-8 min-w-[130px] items-center gap-1.5 rounded-full border border-blue-100 bg-blue-50 px-2 text-xs text-blue-600 dark:border-sky-500/20 dark:bg-sky-500/12 dark:text-sky-200">
|
||||
<span className="h-5 w-5 overflow-hidden rounded-full border border-blue-200 bg-white dark:border-sky-500/30 dark:bg-[#0d1117]">
|
||||
{triggerComposerModel?.avatarUrl ? (
|
||||
<img
|
||||
src={triggerComposerModel.avatarUrl}
|
||||
alt={triggerComposerModel.displayName || "模型头像"}
|
||||
className="h-full w-full object-cover"
|
||||
/>
|
||||
) : (
|
||||
<span className="flex h-full w-full items-center justify-center text-[10px] font-medium text-blue-500">
|
||||
{(triggerComposerModel?.displayName || "A").slice(0, 1)}
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
<span className="max-w-[95px] truncate">
|
||||
{triggerComposerModel?.displayName || "Agent模式"}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="min-w-0 flex-1 overflow-hidden">
|
||||
<ImageParamsBar
|
||||
modelName={triggerComposerModel?.displayName}
|
||||
initialParams={imageParams}
|
||||
onParamsChange={handleImageParamsChange}
|
||||
inline
|
||||
providerType={triggerComposerModel?.providerType}
|
||||
imageGenConfig={triggerComposerModel?.imageGenConfig || undefined}
|
||||
hideImageCount
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-1.5 pl-2">
|
||||
<div className="flex items-center gap-6 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"
|
||||
aria-label="开启 Agent 模式"
|
||||
onClick={onStartAgentMode}
|
||||
title="开启 Agent 模式"
|
||||
>
|
||||
<Lightbulb className="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"
|
||||
title="选择模型"
|
||||
>
|
||||
<span className="inline-flex h-full w-full items-center justify-center">
|
||||
<Box className="h-5 w-5" />
|
||||
</span>
|
||||
</SelectTrigger>
|
||||
<SelectContent
|
||||
align="end"
|
||||
sideOffset={10}
|
||||
className="w-[210px] rounded-[18px] border-black/10 bg-white p-1.5 text-[#20242a] shadow-[0_22px_54px_rgba(15,23,42,0.16)] dark:border-white/10 dark:bg-[#141416] dark:text-white"
|
||||
>
|
||||
{modelOptions.map((model) => (
|
||||
<SelectItem
|
||||
key={model.id}
|
||||
value={model.id}
|
||||
className="h-9 rounded-[12px] text-[13px] [&>span:first-child]:hidden [&>span:last-child]:block [&>span:last-child]:w-full"
|
||||
>
|
||||
<span className="flex min-w-0 items-center gap-2">
|
||||
<span className="flex h-5 w-5 shrink-0 items-center justify-center overflow-hidden rounded-full border border-black/10 bg-white text-[10px] font-medium text-[#6b7280] dark:border-white/15 dark:bg-white/8 dark:text-white/70">
|
||||
{model.avatarUrl ? (
|
||||
<img
|
||||
alt=""
|
||||
className="h-full w-full object-cover"
|
||||
src={model.avatarUrl}
|
||||
/>
|
||||
) : (
|
||||
(model.displayName || "A").slice(0, 1)
|
||||
)}
|
||||
</span>
|
||||
<span className="min-w-0 flex-1 truncate">{model.displayName}</span>
|
||||
</span>
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Button
|
||||
onClick={onSubmit}
|
||||
disabled={isSubmitting || (!prompt.trim() && files.length === 0)}
|
||||
className="h-8 w-8 rounded-full bg-[#9ca3af] p-0 text-white hover:bg-black dark:bg-white/16 dark:hover:bg-white/26"
|
||||
className="h-11 w-11 rounded-full bg-[#eeeeef] p-0 text-[#b9bec7] hover:bg-[#e7e7e8] hover:text-[#8f96a3] dark:bg-white/10 dark:text-white/40 dark:hover:bg-white/16"
|
||||
>
|
||||
{isSubmitting ? (
|
||||
<Loader2 className="h-3.5 w-3.5 animate-spin" />
|
||||
<Loader2 className="h-4.5 w-4.5 animate-spin" />
|
||||
) : (
|
||||
<ArrowUp className="h-3.5 w-3.5" />
|
||||
<ArrowUp className="h-5 w-5" />
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@ -31,7 +31,6 @@ type LovartSidebarProps = {
|
||||
onImageEditor: () => void;
|
||||
onNavLink: (url: string) => void;
|
||||
onNewCanvas: () => void;
|
||||
onProfile: () => void;
|
||||
onRoute: (url: string) => void;
|
||||
onTutorial: (video: TutorialVideo) => void;
|
||||
productPlanningEnabled: boolean;
|
||||
@ -51,6 +50,7 @@ type LovartSidebarProps = {
|
||||
tutorialEnabled: boolean;
|
||||
tutorialVideos: TutorialVideo[];
|
||||
videoCanvasEnabled: boolean;
|
||||
tone?: "light" | "dark";
|
||||
};
|
||||
|
||||
export function LovartSidebar({
|
||||
@ -64,7 +64,6 @@ export function LovartSidebar({
|
||||
onImageEditor,
|
||||
onNavLink,
|
||||
onNewCanvas,
|
||||
onProfile,
|
||||
onRoute,
|
||||
onTutorial,
|
||||
productPlanningEnabled,
|
||||
@ -77,14 +76,15 @@ export function LovartSidebar({
|
||||
tutorialEnabled,
|
||||
tutorialVideos,
|
||||
videoCanvasEnabled,
|
||||
tone = "light",
|
||||
}: LovartSidebarProps) {
|
||||
const sidebarPopupBaseClass =
|
||||
"rounded-[24px] border border-black/8 bg-white/96 p-2.5 text-[#111827] shadow-[0_24px_64px_rgba(15,23,42,0.12)] backdrop-blur-xl";
|
||||
"rounded-[24px] border border-black/8 bg-white/96 p-2.5 text-[#111827] shadow-[0_24px_64px_rgba(15,23,42,0.12)] backdrop-blur-xl dark:border-white/10 dark:bg-[#141416]/96 dark:text-white dark:shadow-[0_24px_64px_rgba(0,0,0,0.42)]";
|
||||
const sidebarButtonClass = (active = false) =>
|
||||
[
|
||||
"group flex h-10 w-10 shrink-0 items-center justify-center rounded-xl text-[#6b7280] transition-all duration-200 hover:bg-black/[0.04] hover:text-[#111827]",
|
||||
"group flex h-[34px] w-[34px] shrink-0 items-center justify-center rounded-[10px] text-[#6b7280] transition-all duration-200 hover:bg-black/[0.04] hover:text-[#111827] dark:text-white/62 dark:hover:bg-white/8 dark:hover:text-white",
|
||||
active
|
||||
? "bg-[#111827] text-white shadow-[0_10px_22px_rgba(15,23,42,0.18)]"
|
||||
? "bg-[#f1f2f4] text-[#111827] shadow-[inset_0_0_0_1px_rgba(15,23,42,0.04)] dark:bg-white/12 dark:text-white"
|
||||
: "",
|
||||
]
|
||||
.filter(Boolean)
|
||||
@ -104,7 +104,10 @@ export function LovartSidebar({
|
||||
onClick={onClick}
|
||||
title={getItem(key).label}
|
||||
>
|
||||
{renderIcon(key, { active: options?.active })}
|
||||
{renderIcon(key, {
|
||||
active: options?.active,
|
||||
sizeClassName: "h-[15px] w-[15px]",
|
||||
})}
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="right">{getItem(key).label}</TooltipContent>
|
||||
@ -124,7 +127,7 @@ export function LovartSidebar({
|
||||
className={sidebarButtonClass()}
|
||||
title={getItem(key).label}
|
||||
>
|
||||
{renderIcon(key)}
|
||||
{renderIcon(key, { sizeClassName: "h-[15px] w-[15px]" })}
|
||||
</button>
|
||||
</HoverCardTrigger>
|
||||
<HoverCardContent
|
||||
@ -132,7 +135,7 @@ export function LovartSidebar({
|
||||
align="start"
|
||||
className={`w-[248px] ${sidebarPopupBaseClass}`}
|
||||
>
|
||||
<div className="px-2 pb-1 pt-1 text-xs font-medium text-black/40">
|
||||
<div className="px-2 pb-1 pt-1 text-xs font-medium text-black/40 dark:text-white/42">
|
||||
{getItem(key).label}
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
@ -140,11 +143,11 @@ export function LovartSidebar({
|
||||
<button
|
||||
key={link.id}
|
||||
type="button"
|
||||
className="flex w-full items-center justify-between rounded-2xl px-3 py-2.5 text-left text-[13px] text-black/80 transition hover:bg-black/[0.04]"
|
||||
className="flex w-full items-center justify-between rounded-2xl px-3 py-2.5 text-left text-[13px] text-black/80 transition hover:bg-black/[0.04] dark:text-white/80 dark:hover:bg-white/8"
|
||||
onClick={() => onNavLink(link.url)}
|
||||
>
|
||||
<span className="line-clamp-1">{link.name}</span>
|
||||
<ArrowRight className="h-3.5 w-3.5 text-black/30" />
|
||||
<ArrowRight className="h-3.5 w-3.5 text-black/30 dark:text-white/35" />
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
@ -153,7 +156,7 @@ export function LovartSidebar({
|
||||
) : null;
|
||||
|
||||
return (
|
||||
<aside className="fixed left-5 top-[88px] z-30 hidden flex-col items-center gap-3 lg:flex">
|
||||
<aside className="fixed left-5 top-[88px] z-30 hidden flex-col items-center gap-2.5 lg:flex">
|
||||
<TooltipProvider delayDuration={120}>
|
||||
{isEnabled("newProject") ? (
|
||||
<Tooltip>
|
||||
@ -161,13 +164,12 @@ export function LovartSidebar({
|
||||
<button
|
||||
type="button"
|
||||
onClick={onNewCanvas}
|
||||
className="flex h-[56px] w-[56px] shrink-0 items-center justify-center rounded-[20px] border border-black/8 bg-white/96 shadow-[0_20px_46px_rgba(15,23,42,0.12)] backdrop-blur-xl transition-transform duration-200 hover:-translate-y-0.5 hover:bg-white"
|
||||
className="flex h-[48px] w-[48px] shrink-0 items-center justify-center rounded-[17px] border border-black/8 bg-white/96 shadow-[0_18px_40px_rgba(15,23,42,0.11)] backdrop-blur-xl transition-transform duration-200 hover:-translate-y-0.5 hover:bg-white dark:border-white/10 dark:bg-[#17181d]/94 dark:shadow-[0_22px_60px_rgba(6,8,14,0.34)] dark:hover:bg-[#202127]"
|
||||
title={getItem("newProject").label}
|
||||
>
|
||||
<div className="flex h-9 w-9 items-center justify-center rounded-[12px] bg-[#111827] text-white shadow-[0_10px_20px_rgba(15,23,42,0.18)]">
|
||||
<div className="flex h-[31px] w-[31px] items-center justify-center rounded-[10px] bg-[#f1f2f4] text-[#111827] shadow-[inset_0_0_0_1px_rgba(15,23,42,0.04)] dark:bg-white/12 dark:text-white">
|
||||
{renderIcon("newProject", {
|
||||
filled: true,
|
||||
sizeClassName: "h-4 w-4",
|
||||
sizeClassName: "h-3.5 w-3.5",
|
||||
})}
|
||||
</div>
|
||||
</button>
|
||||
@ -175,10 +177,12 @@ export function LovartSidebar({
|
||||
<TooltipContent side="right">{getItem("newProject").label}</TooltipContent>
|
||||
</Tooltip>
|
||||
) : null}
|
||||
<FloatingSidebarRail className="max-h-[calc(100vh-172px)]" tone="light">
|
||||
<FloatingSidebarRail
|
||||
className="max-h-[calc(100vh-160px)] !w-[48px] !max-w-[48px] gap-1 !rounded-[20px] !px-[7px] !py-[7px]"
|
||||
tone={tone}
|
||||
>
|
||||
{tooltipButton("home", onHome, { active: !isProjectsOnlyView })}
|
||||
{tooltipButton("projects", onAllProjects, { active: isProjectsOnlyView })}
|
||||
{tooltipButton("profile", onProfile)}
|
||||
{tooltipButton("gallery", onGallery)}
|
||||
{isEnabled("tutorial") && tutorialEnabled ? (
|
||||
<HoverCard openDelay={80} closeDelay={100}>
|
||||
@ -188,7 +192,7 @@ export function LovartSidebar({
|
||||
className={sidebarButtonClass()}
|
||||
title={getItem("tutorial").label}
|
||||
>
|
||||
{renderIcon("tutorial")}
|
||||
{renderIcon("tutorial", { sizeClassName: "h-[15px] w-[15px]" })}
|
||||
</button>
|
||||
</HoverCardTrigger>
|
||||
<HoverCardContent
|
||||
@ -196,7 +200,7 @@ export function LovartSidebar({
|
||||
align="start"
|
||||
className={`w-[300px] ${sidebarPopupBaseClass}`}
|
||||
>
|
||||
<div className="px-2 pb-1 pt-1 text-xs font-medium text-black/40">
|
||||
<div className="px-2 pb-1 pt-1 text-xs font-medium text-black/40 dark:text-white/42">
|
||||
{getItem("tutorial").label}
|
||||
</div>
|
||||
<div className="max-h-[380px] space-y-1 overflow-y-auto">
|
||||
@ -205,10 +209,10 @@ export function LovartSidebar({
|
||||
<button
|
||||
key={video.id}
|
||||
type="button"
|
||||
className="flex w-full items-center gap-2.5 rounded-2xl p-2.5 text-left transition hover:bg-black/[0.04]"
|
||||
className="flex w-full items-center gap-2.5 rounded-2xl p-2.5 text-left transition hover:bg-black/[0.04] dark:hover:bg-white/8"
|
||||
onClick={() => onTutorial(video)}
|
||||
>
|
||||
<div className="h-10 w-16 flex-shrink-0 overflow-hidden rounded-xl bg-black/[0.04]">
|
||||
<div className="h-10 w-16 flex-shrink-0 overflow-hidden rounded-xl bg-black/[0.04] dark:bg-white/8">
|
||||
{video.coverUrl ? (
|
||||
<img
|
||||
src={video.coverUrl}
|
||||
@ -217,16 +221,16 @@ export function LovartSidebar({
|
||||
/>
|
||||
) : (
|
||||
<div className="flex h-full w-full items-center justify-center">
|
||||
<PlayCircle className="h-4 w-4 text-black/35" />
|
||||
<PlayCircle className="h-4 w-4 text-black/35 dark:text-white/45" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="truncate text-[13px] font-medium text-black/85">
|
||||
<div className="truncate text-[13px] font-medium text-black/85 dark:text-white/88">
|
||||
{video.title}
|
||||
</div>
|
||||
{video.description ? (
|
||||
<div className="truncate text-[11px] text-black/42">
|
||||
<div className="truncate text-[11px] text-black/42 dark:text-white/42">
|
||||
{video.description}
|
||||
</div>
|
||||
) : null}
|
||||
@ -234,7 +238,7 @@ export function LovartSidebar({
|
||||
</button>
|
||||
))
|
||||
) : (
|
||||
<div className="px-2 py-3 text-[12px] text-black/42">暂无教程视频</div>
|
||||
<div className="px-2 py-3 text-[12px] text-black/42 dark:text-white/42">暂无教程视频</div>
|
||||
)}
|
||||
</div>
|
||||
</HoverCardContent>
|
||||
|
||||
@ -98,7 +98,7 @@ export type LovartPromptTagVisibility = Record<LovartPromptTagKey, boolean>;
|
||||
|
||||
export const MASONRY_HEIGHTS = [420, 290, 340, 470, 300, 360, 430, 320];
|
||||
export const FEATURED_PAGE_SIZE = 24;
|
||||
export const COMPOSER_PREFIX_DEFAULT = "让Buyi-ai";
|
||||
export const COMPOSER_PREFIX_DEFAULT = "让「由壹点AI」";
|
||||
|
||||
export function normalizeComposerPreviewTail(text: string, fixedPrefix: string) {
|
||||
const trimmed = text.trim();
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Link2, Scan, SlidersHorizontal, Sparkles } from "lucide-react";
|
||||
import { ChevronDown, ChevronUp, Link2, Scan, SlidersHorizontal, Sparkles } from "lucide-react";
|
||||
import { getGrsaiGptImageOutputSize } from "@/lib/ai/grsai-gpt-image-sizes";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Input } from "@/components/ui/input";
|
||||
@ -34,7 +34,7 @@ interface ImageParamsBarProps {
|
||||
providerType?: string; // 模型类型:gemini, volcano-vision, alibaba-image, ruxa-image 等
|
||||
hideImageCount?: boolean; // 隐藏图片数量按钮(仅个别页面需要)
|
||||
hideAdvancedOptions?: boolean; // 隐藏高级设置按钮
|
||||
inlineTheme?: "light" | "dark";
|
||||
inlineTheme?: "light" | "dark" | "minimal";
|
||||
showInlineDivider?: boolean;
|
||||
// 🆕 图片生成模型特性配置(从模型配置中读取)
|
||||
imageGenConfig?: {
|
||||
@ -122,6 +122,10 @@ export function ImageParamsBar({
|
||||
};
|
||||
|
||||
const getImageCountOptions = () => {
|
||||
if (inlineTheme === "minimal") {
|
||||
return Array.from({ length: 10 }, (_, i) => i + 1);
|
||||
}
|
||||
|
||||
if (imageGenConfig?.maxImages) {
|
||||
return Array.from({ length: imageGenConfig.maxImages }, (_, i) => i + 1);
|
||||
}
|
||||
@ -186,17 +190,22 @@ export function ImageParamsBar({
|
||||
|
||||
const buildBaseParams = (source?: Partial<ImageGenerationParams>) =>
|
||||
clampParamsByProvider({
|
||||
aspectRatio: "auto",
|
||||
aspectRatio: inlineTheme === "minimal" ? "1:1" : "auto",
|
||||
imageSize: "2K",
|
||||
imageCount: 1,
|
||||
promptExtend: true,
|
||||
watermark: false,
|
||||
...source,
|
||||
...(inlineTheme === "minimal" && (!source?.aspectRatio || source.aspectRatio === "auto")
|
||||
? { aspectRatio: "1:1" }
|
||||
: {}),
|
||||
});
|
||||
|
||||
const [params, setParams] = useState<ImageGenerationParams>({
|
||||
...buildBaseParams(initialParams),
|
||||
});
|
||||
const [minimalParamsOpen, setMinimalParamsOpen] = useState(false);
|
||||
const [qualityLabel, setQualityLabel] = useState<"自动" | "高" | "中" | "低">("中");
|
||||
|
||||
const imageCountOptions = getImageCountOptions();
|
||||
const showImageCountSelector = !hideImageCount && imageCountOptions.length > 1;
|
||||
@ -300,16 +309,192 @@ export function ImageParamsBar({
|
||||
}
|
||||
};
|
||||
|
||||
const updateParams = (patch: Partial<ImageGenerationParams>) => {
|
||||
const newParams = clampParamsByProvider({ ...params, ...patch });
|
||||
setParams((prevParams) => (isSameParams(prevParams, newParams) ? prevParams : newParams));
|
||||
if (!isSameParams(params, newParams)) {
|
||||
onParamsChange?.(newParams);
|
||||
}
|
||||
};
|
||||
|
||||
// 内联模式:只返回参数按钮,嵌入工具栏
|
||||
if (inline) {
|
||||
const isDarkInline = inlineTheme === "dark";
|
||||
const isMinimalInline = inlineTheme === "minimal";
|
||||
const outputSize = getComputedOutputSize(params.aspectRatio, params.imageSize);
|
||||
|
||||
if (isMinimalInline) {
|
||||
const sizeOptions = [
|
||||
{ label: "1:1", aspectRatio: "1:1", imageSize: "1K" },
|
||||
{ label: "3:2", aspectRatio: "3:2", imageSize: "1K" },
|
||||
{ label: "2:3", aspectRatio: "2:3", imageSize: "1K" },
|
||||
{ label: "4:3", aspectRatio: "4:3", imageSize: "1K" },
|
||||
{ label: "3:4", aspectRatio: "3:4", imageSize: "1K" },
|
||||
{ label: "9:16", aspectRatio: "9:16", imageSize: "1K" },
|
||||
{ label: "1:1(2k)", aspectRatio: "1:1", imageSize: "2K" },
|
||||
{ label: "16:9(2k)", aspectRatio: "16:9", imageSize: "2K" },
|
||||
{ label: "9:16(2k)", aspectRatio: "9:16", imageSize: "2K" },
|
||||
{ label: "16:9(4K)", aspectRatio: "16:9", imageSize: "4K" },
|
||||
{ label: "9:16(4K)", aspectRatio: "9:16", imageSize: "4K" },
|
||||
{ label: "auto", aspectRatio: "auto", imageSize: params.imageSize || "1K" },
|
||||
] as const;
|
||||
const imageCountOptions = Array.from({ length: 10 }, (_, index) => index + 1);
|
||||
const selectedAspectLabel = params.aspectRatio === "auto" ? "auto" : params.aspectRatio;
|
||||
const selectedImageCount = params.imageCount || 1;
|
||||
const renderMinimalRatioGlyph = (ratioValue: string) => {
|
||||
if (ratioValue === "auto") {
|
||||
return <span className="text-[11px] leading-none">auto</span>;
|
||||
}
|
||||
|
||||
const [wRaw, hRaw] = ratioValue.split(":").map((item) => Number(item));
|
||||
if (!wRaw || !hRaw) {
|
||||
return <Scan className="h-4 w-4" />;
|
||||
}
|
||||
const maxSide = 18;
|
||||
let width = maxSide;
|
||||
let height = maxSide;
|
||||
if (wRaw >= hRaw) {
|
||||
width = maxSide;
|
||||
height = Math.max(7, Math.round((maxSide * hRaw) / wRaw));
|
||||
} else {
|
||||
height = maxSide;
|
||||
width = Math.max(7, Math.round((maxSide * wRaw) / hRaw));
|
||||
}
|
||||
|
||||
return (
|
||||
<span
|
||||
className="inline-block rounded-[3px] border border-current"
|
||||
style={{ width, height }}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Popover open={minimalParamsOpen} onOpenChange={setMinimalParamsOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="h-8 shrink-0 rounded-full bg-[#f1f1f2] px-2.5 text-[13px] font-normal text-[#20242a] hover:bg-[#e9eaec] focus-visible:ring-0 focus-visible:ring-offset-0 dark:bg-[#242529] dark:text-zinc-100 dark:hover:bg-[#2c2d33]"
|
||||
title="生成参数"
|
||||
>
|
||||
{qualityLabel} · {selectedAspectLabel} · {selectedImageCount} img
|
||||
{minimalParamsOpen ? (
|
||||
<ChevronUp className="ml-1 h-3 w-3" />
|
||||
) : (
|
||||
<ChevronDown className="ml-1 h-3 w-3" />
|
||||
)}
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent
|
||||
align="center"
|
||||
side="top"
|
||||
sideOffset={8}
|
||||
className="w-[304px] max-w-[calc(100vw-32px)] rounded-[18px] border border-[#c7ccd5] bg-white p-3.5 text-[#252b33] shadow-[0_22px_56px_rgba(15,23,42,0.16)] dark:border-white/12 dark:bg-[#141416] dark:text-zinc-100 dark:shadow-[0_24px_64px_rgba(0,0,0,0.42)]"
|
||||
>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<div className="mb-2 text-[14px] font-medium leading-none">质量</div>
|
||||
<div className="grid h-9 grid-cols-4 rounded-full bg-[#f4f4f5] p-0.5 dark:bg-white/8">
|
||||
{(["自动", "高", "中", "低"] as const).map((quality) => (
|
||||
<button
|
||||
key={quality}
|
||||
type="button"
|
||||
className={cn(
|
||||
"rounded-full text-[13px] font-medium transition-colors",
|
||||
qualityLabel === quality
|
||||
? "border border-[#c8cdd5] bg-white shadow-[0_1px_4px_rgba(15,23,42,0.1)] dark:border-white/12 dark:bg-white/16"
|
||||
: "text-[#3d434d] hover:bg-white/55 dark:text-zinc-300 dark:hover:bg-white/10"
|
||||
)}
|
||||
onClick={() => setQualityLabel(quality)}
|
||||
>
|
||||
{quality}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="mb-2 text-[14px] font-medium leading-none">尺寸</div>
|
||||
<div className="grid grid-cols-[1fr_20px_1fr] items-center gap-2">
|
||||
<div className="flex h-10 items-center gap-2 rounded-[10px] bg-[#f3f3f4] px-3 text-[15px] dark:bg-white/8">
|
||||
<span className="text-[#68707c] dark:text-zinc-400">W</span>
|
||||
<span>{outputSize.width}</span>
|
||||
</div>
|
||||
<Link2 className="mx-auto h-3.5 w-3.5 text-[#8b929c]" />
|
||||
<div className="flex h-10 items-center gap-2 rounded-[10px] bg-[#f3f3f4] px-3 text-[15px] dark:bg-white/8">
|
||||
<span className="text-[#68707c] dark:text-zinc-400">H</span>
|
||||
<span>{outputSize.height}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="mb-2 text-[14px] font-medium leading-none">Size</div>
|
||||
<div className="grid grid-cols-4 gap-2">
|
||||
{sizeOptions.map((option) => {
|
||||
const selected =
|
||||
params.aspectRatio === option.aspectRatio &&
|
||||
(option.aspectRatio === "auto" || params.imageSize === option.imageSize);
|
||||
return (
|
||||
<button
|
||||
key={`${option.aspectRatio}-${option.imageSize}-${option.label}`}
|
||||
type="button"
|
||||
className={cn(
|
||||
"flex h-[66px] flex-col items-center justify-center gap-1.5 rounded-[10px] border text-[13px] transition-colors",
|
||||
selected
|
||||
? "border-[#bfc4cc] bg-[#e4e4e5] text-[#171b21] dark:border-white/18 dark:bg-white/16 dark:text-zinc-50"
|
||||
: "border-[#d0d3d9] bg-white text-[#4c535d] hover:bg-[#f5f5f6] dark:border-white/12 dark:bg-transparent dark:text-zinc-300 dark:hover:bg-white/8"
|
||||
)}
|
||||
onClick={() =>
|
||||
updateParams({
|
||||
aspectRatio: option.aspectRatio,
|
||||
imageSize: option.imageSize,
|
||||
})
|
||||
}
|
||||
>
|
||||
<span className="flex h-5 items-center justify-center text-[#30353c] dark:text-zinc-200">
|
||||
{renderMinimalRatioGlyph(option.aspectRatio)}
|
||||
</span>
|
||||
<span>{option.label}</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="mb-2 text-[14px] font-medium leading-none">Image</div>
|
||||
<div className="grid grid-cols-4 gap-2">
|
||||
{imageCountOptions.map((count) => (
|
||||
<button
|
||||
key={count}
|
||||
type="button"
|
||||
className={cn(
|
||||
"h-9 rounded-[9px] border text-[14px] transition-colors",
|
||||
selectedImageCount === count
|
||||
? "border-[#bfc4cc] bg-[#e4e4e5] text-[#171b21] dark:border-white/18 dark:bg-white/16 dark:text-zinc-50"
|
||||
: "border-[#d0d3d9] bg-white text-[#4c535d] hover:bg-[#f5f5f6] dark:border-white/12 dark:bg-transparent dark:text-zinc-300 dark:hover:bg-white/8"
|
||||
)}
|
||||
onClick={() => updateParams({ imageCount: count })}
|
||||
>
|
||||
{count} img
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"ml-1 flex min-w-0 items-center gap-1 pl-1",
|
||||
"flex min-w-0 items-center",
|
||||
isMinimalInline ? "ml-0 gap-2 pl-0" : "ml-1 gap-1 pl-1",
|
||||
showInlineDivider &&
|
||||
!isMinimalInline &&
|
||||
(isDarkInline ? "border-l border-white/10" : "border-l border-border/50")
|
||||
)}
|
||||
>
|
||||
@ -318,10 +503,15 @@ export function ImageParamsBar({
|
||||
<Button
|
||||
variant="ghost"
|
||||
className={cn(
|
||||
"h-8 w-11 shrink-0 rounded-[12px] p-0",
|
||||
isDarkInline
|
||||
"shrink-0 p-0",
|
||||
isMinimalInline
|
||||
? "h-11 w-11 rounded-full border border-transparent bg-transparent text-[#555b66] hover:bg-[#f3f4f6] hover:text-[#20242a]"
|
||||
: "h-8 w-11 rounded-[12px]",
|
||||
!isMinimalInline && isDarkInline
|
||||
? "border border-white/12 bg-white/[0.06] text-white hover:bg-white/[0.1]"
|
||||
: "border border-[#d6dbe2] bg-[#f3f4f6]/90 text-[#2f3b48] hover:bg-[#edeff2]/90"
|
||||
: !isMinimalInline
|
||||
? "border border-[#d6dbe2] bg-[#f3f4f6]/90 text-[#2f3b48] hover:bg-[#edeff2]/90"
|
||||
: ""
|
||||
)}
|
||||
aria-label={`比例:${params.aspectRatio},分辨率:${params.imageSize}`}
|
||||
title={`比例:${params.aspectRatio},分辨率:${params.imageSize}`}
|
||||
@ -329,7 +519,11 @@ export function ImageParamsBar({
|
||||
<span
|
||||
className={cn(
|
||||
"flex h-5 w-5 items-center justify-center",
|
||||
isDarkInline ? "text-white/80" : "text-[#20262f]"
|
||||
isMinimalInline
|
||||
? "text-current"
|
||||
: isDarkInline
|
||||
? "text-white/80"
|
||||
: "text-[#20262f]"
|
||||
)}
|
||||
>
|
||||
{renderRatioGlyph(params.aspectRatio)}
|
||||
@ -444,15 +638,33 @@ export function ImageParamsBar({
|
||||
<Button
|
||||
variant="ghost"
|
||||
className={cn(
|
||||
"h-8 min-w-[56px] rounded-[12px] px-2.5 text-[12px] font-semibold focus-visible:ring-0 focus-visible:ring-offset-0",
|
||||
isDarkInline
|
||||
"text-[12px] font-semibold focus-visible:ring-0 focus-visible:ring-offset-0",
|
||||
isMinimalInline
|
||||
? "h-11 min-w-11 rounded-full border border-transparent bg-transparent px-0 text-[#555b66] hover:bg-[#f3f4f6] hover:text-[#20242a]"
|
||||
: "h-8 min-w-[56px] rounded-[12px] px-2.5",
|
||||
!isMinimalInline && isDarkInline
|
||||
? "border border-white/12 bg-white/[0.06] text-white hover:bg-white/[0.1]"
|
||||
: "border border-[#d6dbe2] bg-[#f3f4f6]/90 text-[#2f3b48] hover:bg-[#edeff2]/90"
|
||||
: !isMinimalInline
|
||||
? "border border-[#d6dbe2] bg-[#f3f4f6]/90 text-[#2f3b48] hover:bg-[#edeff2]/90"
|
||||
: ""
|
||||
)}
|
||||
title={`数量:${params.imageCount || 1}张`}
|
||||
>
|
||||
<Sparkles className={cn("mr-1 h-3.5 w-3.5", isDarkInline ? "text-emerald-300" : "text-[#6b7280]")} />
|
||||
{params.imageCount || 1}张
|
||||
<Sparkles
|
||||
className={cn(
|
||||
isMinimalInline ? "h-5 w-5" : "mr-1 h-3.5 w-3.5",
|
||||
isMinimalInline
|
||||
? "text-current"
|
||||
: isDarkInline
|
||||
? "text-emerald-300"
|
||||
: "text-[#6b7280]"
|
||||
)}
|
||||
/>
|
||||
{isMinimalInline ? (
|
||||
<span className="sr-only">{params.imageCount || 1}张</span>
|
||||
) : (
|
||||
`${params.imageCount || 1}张`
|
||||
)}
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent
|
||||
@ -505,11 +717,21 @@ export function ImageParamsBar({
|
||||
<Button
|
||||
variant="ghost"
|
||||
className={cn(
|
||||
"h-8 w-8 shrink-0 rounded-[12px] p-0 focus-visible:ring-0 focus-visible:ring-offset-0",
|
||||
isDarkInline
|
||||
"shrink-0 p-0 focus-visible:ring-0 focus-visible:ring-offset-0",
|
||||
isMinimalInline
|
||||
? "h-11 w-11 rounded-full border border-transparent bg-transparent text-[#555b66] hover:bg-[#f3f4f6] hover:text-[#20242a]"
|
||||
: "h-8 w-8 rounded-[12px]",
|
||||
!isMinimalInline && isDarkInline
|
||||
? "border border-white/12 bg-white/[0.06] text-white/70 hover:bg-white/[0.1]"
|
||||
: "border border-[#d6dbe2] bg-[#f3f4f6]/90 text-[#5f6b7a] hover:bg-[#edeff2]/90",
|
||||
(params.promptExtend || params.negativePrompt) && (isDarkInline ? "text-emerald-300" : "text-primary")
|
||||
: !isMinimalInline
|
||||
? "border border-[#d6dbe2] bg-[#f3f4f6]/90 text-[#5f6b7a] hover:bg-[#edeff2]/90"
|
||||
: "",
|
||||
(params.promptExtend || params.negativePrompt) &&
|
||||
(isMinimalInline
|
||||
? "text-[#20242a]"
|
||||
: isDarkInline
|
||||
? "text-emerald-300"
|
||||
: "text-primary")
|
||||
)}
|
||||
title="高级设置"
|
||||
>
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
-- These columns are referenced by /api/site-settings and admin settings UI.
|
||||
|
||||
ALTER TABLE "SiteSettings" ADD COLUMN IF NOT EXISTS "bannerEnabled" boolean NOT NULL DEFAULT true;
|
||||
ALTER TABLE "SiteSettings" ADD COLUMN IF NOT EXISTS "bannerText" text DEFAULT '升级会员, Nano Banana Pro 免费365天!';
|
||||
ALTER TABLE "SiteSettings" ADD COLUMN IF NOT EXISTS "bannerText" text DEFAULT '限时特惠:升级会员最高立享 45% OFF!';
|
||||
ALTER TABLE "SiteSettings" ADD COLUMN IF NOT EXISTS "bannerLink" text;
|
||||
|
||||
ALTER TABLE "SiteSettings" ADD COLUMN IF NOT EXISTS "tutorialVideoUrl" text;
|
||||
@ -17,4 +17,3 @@ ALTER TABLE "SiteSettings" ADD COLUMN IF NOT EXISTS "canvasAutoModeDefaultModelI
|
||||
INSERT INTO "SiteSettings" ("siteName")
|
||||
SELECT 'AI Chatbot'
|
||||
WHERE NOT EXISTS (SELECT 1 FROM "SiteSettings");
|
||||
|
||||
|
||||
@ -4727,7 +4727,7 @@ export async function getSiteSettings() {
|
||||
favicon: null,
|
||||
defaultTheme: "system",
|
||||
bannerEnabled: true,
|
||||
bannerText: "升级会员, Nano Banana Pro 免费365天!",
|
||||
bannerText: "限时特惠:升级会员最高立享 45% OFF!",
|
||||
bannerLink: null,
|
||||
toolbox1Name: "工具箱",
|
||||
toolbox1Enabled: true,
|
||||
|
||||
@ -457,7 +457,7 @@ export const siteSettings = pgTable("SiteSettings", {
|
||||
usageTimeMessage: text("usageTimeMessage").default("由壹点累了也是需要休息的"), // 超时提示词
|
||||
// 广告横幅设置
|
||||
bannerEnabled: boolean("bannerEnabled").notNull().default(true), // 是否显示广告横幅
|
||||
bannerText: text("bannerText").default("升级会员, Nano Banana Pro 免费365天!"), // 广告横幅文字
|
||||
bannerText: text("bannerText").default("限时特惠:升级会员最高立享 45% OFF!"), // 广告横幅文字
|
||||
bannerLink: text("bannerLink"), // 广告横幅链接(可选)
|
||||
// 工具箱设置
|
||||
toolbox1Name: varchar("toolbox1Name", { length: 64 }).default("工具箱"), // 工具箱1名称
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user