From ef45606a24e98e6929e07c604e9c1ae640389297 Mon Sep 17 00:00:00 2001 From: liangxu Date: Wed, 8 Jul 2026 09:00:20 +0800 Subject: [PATCH] feat(home): animated typewriter placeholder for the prompt composer Cycle through localized example prompts with a typing/deleting animation in the home composer placeholder, start the field empty instead of with a default prompt, and style the home editor with larger prompt text. Also rebrand the device-management copy from Lovart to Moteva. Co-Authored-By: Claude Opus 4.8 (1M context) --- frontend/src/i18n/i18n.tsx | 4 +- frontend/src/ui/pages/HomePage/index.tsx | 90 +++++++++++++++++++++++- frontend/src/ui/styles.css | 11 +++ 3 files changed, 101 insertions(+), 4 deletions(-) diff --git a/frontend/src/i18n/i18n.tsx b/frontend/src/i18n/i18n.tsx index b9f38b2..5da6c67 100644 --- a/frontend/src/i18n/i18n.tsx +++ b/frontend/src/i18n/i18n.tsx @@ -29,7 +29,7 @@ const dictionaries: Record = { email: "电子邮箱", deviceManagement: "设备管理", removeAllDevices: "移除全部设备", - removeAllDevicesDescription: "Lovart 账户仅限个人使用。为防止滥用并确保平台稳定,任务最多可同时在 2 个桌面 Web 会话和 1 个移动 Web 会话中运行。如需团队或多人使用,请购买 Team Plan。", + removeAllDevicesDescription: "Moteva 账户仅限个人使用。为防止滥用并确保平台稳定,任务最多可同时在 2 个桌面 Web 会话和 1 个移动 Web 会话中运行。如需团队或多人使用,请购买 Team Plan。", onlineStatus: "在线状态", deviceInfo: "设备信息", currentDevice: "当前设备", @@ -490,7 +490,7 @@ const dictionaries: Record = { email: "Email", deviceManagement: "Device Management", removeAllDevices: "Remove All Devices", - removeAllDevicesDescription: "A Lovart account is for personal use only. To prevent abuse and keep the platform stable, tasks can run in up to 2 desktop web sessions and 1 mobile web session at the same time. For team or multi-person use, please purchase Team Plan.", + removeAllDevicesDescription: "A Moteva account is for personal use only. To prevent abuse and keep the platform stable, tasks can run in up to 2 desktop web sessions and 1 mobile web session at the same time. For team or multi-person use, please purchase Team Plan.", onlineStatus: "Online", deviceInfo: "Device Info", currentDevice: "Current Device", diff --git a/frontend/src/ui/pages/HomePage/index.tsx b/frontend/src/ui/pages/HomePage/index.tsx index fde388a..67a0e38 100644 --- a/frontend/src/ui/pages/HomePage/index.tsx +++ b/frontend/src/ui/pages/HomePage/index.tsx @@ -75,6 +75,41 @@ type HomeImageModel = { credits: number; }; +type HomePromptPlaceholderBlueprint = { + prefix: string; + phrases: string[]; +}; + +type TypewriterPhase = "typing" | "deleting" | "pausing"; + +const homePromptPlaceholderBlueprints: Record = { + "zh-CN": { + prefix: "让 Moteva ", + phrases: [ + "设计一个令人惊艳的标志", + "创建一个温馨的咖啡厅风格菜单海报", + "生成一组水彩电影感插画分镜", + "为一家日系面包店设计完整品牌视觉", + "制作一张高转化的电商产品图" + ] + }, + "en-US": { + prefix: "Ask Moteva to ", + phrases: [ + "design a striking logo", + "create a warm cafe-style menu poster", + "generate a watercolor cinematic storyboard", + "build a complete brand identity for a Japanese bakery", + "make a high-converting e-commerce product visual" + ] + } +}; + +const HOME_PLACEHOLDER_TYPE_DELAY_MS = 48; +const HOME_PLACEHOLDER_DELETE_DELAY_MS = 24; +const HOME_PLACEHOLDER_HOLD_DELAY_MS = 1600; +const HOME_PLACEHOLDER_PAUSE_DELAY_MS = 260; + const promptModeBlueprints = [ { id: "poster", @@ -519,6 +554,55 @@ function hasPromptText(value: string) { return value.replace(/\u00a0/g, " ").trim().length > 0; } +function useHomePromptPlaceholder(locale: Locale) { + const blueprint = homePromptPlaceholderBlueprints[locale]; + const [phraseIndex, setPhraseIndex] = useState(0); + const [visibleCharacters, setVisibleCharacters] = useState(0); + const [phase, setPhase] = useState("typing"); + const phrase = blueprint.phrases[phraseIndex] ?? blueprint.phrases[0] ?? ""; + const characters = useMemo(() => Array.from(phrase), [phrase]); + + useEffect(() => { + setPhraseIndex(0); + setVisibleCharacters(0); + setPhase("typing"); + }, [locale]); + + useEffect(() => { + if (phase === "typing") { + if (visibleCharacters < characters.length) { + const timeoutId = window.setTimeout(() => { + setVisibleCharacters((count) => Math.min(count + 1, characters.length)); + }, HOME_PLACEHOLDER_TYPE_DELAY_MS); + return () => window.clearTimeout(timeoutId); + } + + const timeoutId = window.setTimeout(() => setPhase("deleting"), HOME_PLACEHOLDER_HOLD_DELAY_MS); + return () => window.clearTimeout(timeoutId); + } + + if (phase === "deleting") { + if (visibleCharacters > 0) { + const timeoutId = window.setTimeout(() => { + setVisibleCharacters((count) => Math.max(count - 1, 0)); + }, HOME_PLACEHOLDER_DELETE_DELAY_MS); + return () => window.clearTimeout(timeoutId); + } + + const timeoutId = window.setTimeout(() => setPhase("pausing"), HOME_PLACEHOLDER_PAUSE_DELAY_MS); + return () => window.clearTimeout(timeoutId); + } + + const timeoutId = window.setTimeout(() => { + setPhraseIndex((index) => (index + 1) % blueprint.phrases.length); + setPhase("typing"); + }, HOME_PLACEHOLDER_PAUSE_DELAY_MS); + return () => window.clearTimeout(timeoutId); + }, [blueprint, characters.length, phase, visibleCharacters]); + + return `${blueprint.prefix}${characters.slice(0, visibleCharacters).join("")}`; +} + type CreateProjectOptions = { allowEmptyPrompt?: boolean; }; @@ -535,7 +619,7 @@ export function HomePage() { const promptComposerRef = useRef(null); const referenceImagesRef = useRef([]); const [dashboard, setDashboard] = useState(null); - const [prompt, setPrompt] = useState(() => t("defaultPrompt")); + const [prompt, setPrompt] = useState(""); const [referenceImages, setReferenceImages] = useState([]); const [activeModeId, setActiveModeId] = useState("poster"); const [activeInspirationCategory, setActiveInspirationCategory] = useState("all"); @@ -571,6 +655,7 @@ export function HomePage() { const selectedModel = imageModels.find((model) => model.id === selectedModelId) ?? imageModels[0]; const inspirationCategories = useMemo(() => getInspirationCategories(locale), [locale]); const inspirationCatalog = useMemo(() => getInspirationCatalog(locale), [locale]); + const promptPlaceholder = useHomePromptPlaceholder(locale); const activeMode = promptModes.find((mode) => mode.id === activeModeId) ?? promptModes[0]; const projects = dashboard?.projects ?? []; const canSubmitPrompt = hasPromptText(prompt); @@ -728,8 +813,9 @@ export function HomePage() {