feat: add dynamic canvas placeholder with shared admin config
This commit is contained in:
parent
a4dca610aa
commit
0c44d498d1
@ -3781,9 +3781,9 @@ export function AdminSettingsClient() {
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid gap-4 rounded-lg border border-dashed p-4">
|
||||
<div className="text-sm font-medium">新版首页文案配置</div>
|
||||
<div className="text-sm font-medium">首页 / 无限画布对话框文案配置</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="lovartComposerPrefix">对话框固定前缀文案</Label>
|
||||
<Label htmlFor="lovartComposerPrefix">对话框固定前缀文案(首页与无限画布共用)</Label>
|
||||
<Input
|
||||
id="lovartComposerPrefix"
|
||||
value={siteSettings.lovartComposerPrefix}
|
||||
@ -3801,7 +3801,7 @@ export function AdminSettingsClient() {
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="lovartComposerPreviewTexts">
|
||||
对话框预览文案(每行一条,按顺序轮播打字显示)
|
||||
对话框预览文案(首页与无限画布共用,每行一条,按顺序轮播打字显示)
|
||||
</Label>
|
||||
<Textarea
|
||||
id="lovartComposerPreviewTexts"
|
||||
|
||||
@ -89,6 +89,32 @@ interface CanvasChatPanelProps {
|
||||
|
||||
// 建议卡片数据 - 已移除
|
||||
const SUGGESTIONS: { title: string; desc: string; image: string }[] = [];
|
||||
const CANVAS_COMPOSER_PREFIX_DEFAULT = "让Buyi-ai";
|
||||
const CANVAS_COMPOSER_PREVIEW_TEXTS_DEFAULT = [
|
||||
"打造引人注目的社交媒体视觉",
|
||||
"帮我把这张产品图做成高级电商海报",
|
||||
"基于参考图延展 3 套同风格方案",
|
||||
];
|
||||
|
||||
function normalizeComposerPreviewTail(text: string, fixedPrefix: string) {
|
||||
const trimmed = text.trim();
|
||||
if (!trimmed) return "";
|
||||
|
||||
const sanitizedPrefix = fixedPrefix.trim();
|
||||
if (sanitizedPrefix && trimmed.startsWith(sanitizedPrefix)) {
|
||||
return trimmed.slice(sanitizedPrefix.length).trimStart();
|
||||
}
|
||||
|
||||
const legacyPrefixRemoved = trimmed
|
||||
.replace(/^让\s*buyi[-_ ]?ai\s*/i, "")
|
||||
.replace(/^让\s*lovart\s*/i, "")
|
||||
.trimStart();
|
||||
if (legacyPrefixRemoved !== trimmed) {
|
||||
return legacyPrefixRemoved;
|
||||
}
|
||||
|
||||
return trimmed;
|
||||
}
|
||||
|
||||
// 获取图片尺寸(保留原图分辨率 + 画布显示尺寸)
|
||||
function getImageDimensions(url: string): Promise<{
|
||||
@ -2248,6 +2274,8 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF
|
||||
bannerEnabled?: boolean;
|
||||
bannerText?: string;
|
||||
bannerLink?: string | null;
|
||||
lovartComposerPrefix?: string | null;
|
||||
lovartComposerPreviewTexts?: string[];
|
||||
canvasEnabled?: boolean;
|
||||
aiDetailGenerationEnabled?: boolean;
|
||||
canvasDefaultModelId?: string | null;
|
||||
@ -2292,6 +2320,8 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF
|
||||
}
|
||||
return "";
|
||||
}, [session?.user?.email, session?.user?.name]);
|
||||
const [canvasTypedPreviewText, setCanvasTypedPreviewText] = useState("");
|
||||
const [canvasPreviewCursorVisible, setCanvasPreviewCursorVisible] = useState(true);
|
||||
const [greetingPeriodText, setGreetingPeriodText] = useState("中午好!");
|
||||
useEffect(() => {
|
||||
const hour = new Date().getHours();
|
||||
@ -2309,6 +2339,95 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF
|
||||
}
|
||||
setGreetingPeriodText("深夜好!");
|
||||
}, []);
|
||||
const canvasComposerPrefix = useMemo(
|
||||
() =>
|
||||
String(siteSettings?.lovartComposerPrefix || "").trim() ||
|
||||
CANVAS_COMPOSER_PREFIX_DEFAULT,
|
||||
[siteSettings?.lovartComposerPrefix]
|
||||
);
|
||||
const canvasComposerPreviewTexts = useMemo(() => {
|
||||
const configured = (siteSettings?.lovartComposerPreviewTexts || [])
|
||||
.map((item) => String(item || "").trim())
|
||||
.filter(Boolean);
|
||||
return configured.length > 0
|
||||
? configured
|
||||
: CANVAS_COMPOSER_PREVIEW_TEXTS_DEFAULT;
|
||||
}, [siteSettings?.lovartComposerPreviewTexts]);
|
||||
const canvasComposerPreviewTails = useMemo(
|
||||
() =>
|
||||
canvasComposerPreviewTexts
|
||||
.map((text) => normalizeComposerPreviewTail(text, canvasComposerPrefix))
|
||||
.filter(Boolean),
|
||||
[canvasComposerPrefix, canvasComposerPreviewTexts]
|
||||
);
|
||||
useEffect(() => {
|
||||
let disposed = false;
|
||||
let timer: ReturnType<typeof setTimeout> | null = null;
|
||||
|
||||
if (canvasComposerPreviewTails.length === 0) {
|
||||
setCanvasTypedPreviewText("");
|
||||
return () => {
|
||||
if (timer) clearTimeout(timer);
|
||||
};
|
||||
}
|
||||
|
||||
let textIndex = 0;
|
||||
let charIndex = 0;
|
||||
|
||||
const typeForward = () => {
|
||||
if (disposed) return;
|
||||
const currentText = canvasComposerPreviewTails[textIndex] || "";
|
||||
|
||||
if (charIndex < currentText.length) {
|
||||
charIndex += 1;
|
||||
setCanvasTypedPreviewText(currentText.slice(0, charIndex));
|
||||
timer = setTimeout(typeForward, 55);
|
||||
return;
|
||||
}
|
||||
|
||||
if (canvasComposerPreviewTails.length === 1) {
|
||||
setCanvasTypedPreviewText(currentText);
|
||||
return;
|
||||
}
|
||||
|
||||
timer = setTimeout(typeBackward, 1200);
|
||||
};
|
||||
|
||||
const typeBackward = () => {
|
||||
if (disposed) return;
|
||||
const currentText = canvasComposerPreviewTails[textIndex] || "";
|
||||
|
||||
if (charIndex > 0) {
|
||||
charIndex -= 1;
|
||||
setCanvasTypedPreviewText(currentText.slice(0, charIndex));
|
||||
timer = setTimeout(typeBackward, 35);
|
||||
return;
|
||||
}
|
||||
|
||||
textIndex = (textIndex + 1) % canvasComposerPreviewTails.length;
|
||||
timer = setTimeout(typeForward, 260);
|
||||
};
|
||||
|
||||
setCanvasTypedPreviewText("");
|
||||
timer = setTimeout(typeForward, 260);
|
||||
|
||||
return () => {
|
||||
disposed = true;
|
||||
if (timer) clearTimeout(timer);
|
||||
};
|
||||
}, [canvasComposerPreviewTails]);
|
||||
|
||||
useEffect(() => {
|
||||
const blinkTimer = setInterval(() => {
|
||||
setCanvasPreviewCursorVisible((value) => !value);
|
||||
}, 450);
|
||||
|
||||
return () => clearInterval(blinkTimer);
|
||||
}, []);
|
||||
|
||||
const canvasDynamicPlaceholder = `${canvasComposerPrefix}${canvasTypedPreviewText}${
|
||||
canvasPreviewCursorVisible ? " |" : ""
|
||||
}`;
|
||||
const totalGeneratedImages = Number(userImageStats?.totalImages ?? NaN);
|
||||
const aiDetailGenerationEnabled = siteSettings?.aiDetailGenerationEnabled === true;
|
||||
const shouldShowCanvasBanner =
|
||||
@ -6008,7 +6127,7 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF
|
||||
: selectedSkillTag?.placeholder?.trim() ||
|
||||
(selectedSkillTag
|
||||
? `请输入「${selectedSkillTag.name}」相关需求`
|
||||
: "请输入你的设计需求,例如:绿色椅子 替换 Image");
|
||||
: 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;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user