|
|
@@ -48,6 +48,7 @@ import type { Message, EnergyStoneSpawnMessage, GreetingContextMessage } from '@
|
|
|
import '@/styles/chatbot-fix.css';
|
|
|
import { getOssUrl } from '@/utils/oss';
|
|
|
import { wrapTrackedClick, trackButtonClick } from '@/utils/tracking';
|
|
|
+import { STOOL_LAYOUT } from '@/constants/chatbot';
|
|
|
|
|
|
const tapToChat = getOssUrl('TapToChat.svg');
|
|
|
const cancelIcon = getOssUrl('cancel-icon.png');
|
|
|
@@ -65,6 +66,12 @@ const chatBgTop = getOssUrl('chat-bg-top.svg');
|
|
|
const chatBgRight = getOssUrl('chat-bg-right.svg');
|
|
|
const chatBgBottom = getOssUrl('chat-bg-bottom.svg');
|
|
|
const chatBgLeft = getOssUrl('chat-bg-left.svg');
|
|
|
+// 特殊场景(窗前看风景/桌前看书/桌前吃饭)凳子上展示的动画形象
|
|
|
+const SPECIAL_SCENE_GIF: Record<'window' | 'reading' | 'eating', string> = {
|
|
|
+ window: getOssUrl('lumi-windows.gif'),
|
|
|
+ reading: getOssUrl('lumi-book.gif'),
|
|
|
+ eating: getOssUrl('lumi-eat.gif'),
|
|
|
+};
|
|
|
const settingIcon = getOssUrl('setting-icon.png');
|
|
|
const funcLockIcon = getOssUrl('func-Lock.png');
|
|
|
const profileMenuIcon = getOssUrl('profile-icon.png');
|
|
|
@@ -206,6 +213,8 @@ export const ChatBotPage = () => {
|
|
|
// 情景对话:是否已经在当前场景当前话题下真正开始对话(首条语音已发送)
|
|
|
const [scenarioConversationStarted, setScenarioConversationStarted] = useState(false);
|
|
|
const hasSentScenarioContextRef = useRef(false);
|
|
|
+ // 特殊场景:用户点击录音后收起凳子上形象、恢复中间动画形象
|
|
|
+ const [specialSceneAvatarDismissed, setSpecialSceneAvatarDismissed] = useState(false);
|
|
|
const greetingContextRef = useRef(greetingContext);
|
|
|
useEffect(() => {
|
|
|
greetingContextRef.current = greetingContext;
|
|
|
@@ -2269,6 +2278,7 @@ export const ChatBotPage = () => {
|
|
|
setCurrentTopicIndex(idx >= 0 ? idx : 0);
|
|
|
hasSentScenarioContextRef.current = false;
|
|
|
setScenarioConversationStarted(false);
|
|
|
+ setSpecialSceneAvatarDismissed(false); // 进入特殊场景时展示凳子上形象
|
|
|
if (ctx.firstLine) setRecognizedText(ctx.firstLine);
|
|
|
} else {
|
|
|
setGreetingContext(null);
|
|
|
@@ -2586,6 +2596,11 @@ export const ChatBotPage = () => {
|
|
|
|
|
|
// 如果正在连接中或已就绪,允许开始录音(消息会被缓冲)
|
|
|
|
|
|
+ // 特殊场景:点击录音后收起凳子上的场景形象,恢复中间动画形象
|
|
|
+ if (greetingContext?.scene && greetingContext.scene !== 'default') {
|
|
|
+ setSpecialSceneAvatarDismissed(true);
|
|
|
+ }
|
|
|
+
|
|
|
// 一旦用户主动开始录音,则以用户为准:后续所有首句 greeting 消息全部丢弃
|
|
|
greetingSuppressedRef.current = true;
|
|
|
// 如果此时已经拿到首句的 requestId,则主动向后端发送 cancel,节省资源
|
|
|
@@ -2885,7 +2900,7 @@ export const ChatBotPage = () => {
|
|
|
|
|
|
// 根据背景图片获取对应的树木装饰图片URL(/chatbot 界面统一使用默认图片,不随用户选择的新年等背景切换)
|
|
|
const getTreesImage = (): string | null => {
|
|
|
- return getOssUrl('init2.svg');
|
|
|
+ return getOssUrl('init3.svg');
|
|
|
};
|
|
|
|
|
|
// 根据背景图片ID获取树木装饰图片的样式配置(/chatbot 使用默认背景,透明度固定为 1)
|
|
|
@@ -2929,6 +2944,48 @@ export const ChatBotPage = () => {
|
|
|
/>
|
|
|
)}
|
|
|
|
|
|
+ {/* 凳子 + 特殊场景 GIF 均基于 STOOL_LAYOUT,改凳子位置时两处同步 */}
|
|
|
+ <img
|
|
|
+ src={getOssUrl('stool.png')}
|
|
|
+ alt=""
|
|
|
+ className="absolute pointer-events-none"
|
|
|
+ style={{
|
|
|
+ width: px2vmin(STOOL_LAYOUT.width),
|
|
|
+ height: px2vmin(STOOL_LAYOUT.height),
|
|
|
+ left: px2vmin(STOOL_LAYOUT.left),
|
|
|
+ top: px2vmin(STOOL_LAYOUT.top),
|
|
|
+ zIndex: 2,
|
|
|
+ objectFit: 'contain',
|
|
|
+ }}
|
|
|
+ draggable={false}
|
|
|
+ />
|
|
|
+
|
|
|
+ {/* 特殊场景:凳子上展示的场景动画形象,位置基于凳子(居中于凳子、底边对齐凳子底) */}
|
|
|
+ {greetingContext?.scene &&
|
|
|
+ greetingContext.scene !== 'default' &&
|
|
|
+ !specialSceneAvatarDismissed &&
|
|
|
+ SPECIAL_SCENE_GIF[greetingContext.scene] && (
|
|
|
+ <img
|
|
|
+ src={SPECIAL_SCENE_GIF[greetingContext.scene]}
|
|
|
+ alt=""
|
|
|
+ className="absolute pointer-events-none"
|
|
|
+ style={{
|
|
|
+ width: px2vmin(STOOL_LAYOUT.sceneGifSize),
|
|
|
+ height: px2vmin(STOOL_LAYOUT.sceneGifSize),
|
|
|
+ left: px2vmin(STOOL_LAYOUT.left + STOOL_LAYOUT.width / 2 - STOOL_LAYOUT.sceneGifSize / 2),
|
|
|
+ top: px2vmin(
|
|
|
+ STOOL_LAYOUT.top +
|
|
|
+ STOOL_LAYOUT.height -
|
|
|
+ STOOL_LAYOUT.sceneGifSize +
|
|
|
+ (STOOL_LAYOUT.sceneGifOffsetYByScene[greetingContext.scene] ?? STOOL_LAYOUT.sceneGifOffsetY)
|
|
|
+ ),
|
|
|
+ zIndex: 2,
|
|
|
+ objectFit: 'contain',
|
|
|
+ }}
|
|
|
+ draggable={false}
|
|
|
+ />
|
|
|
+ )}
|
|
|
+
|
|
|
{/* 说话时的阴影蒙层:仅录音中出现,发送/取消后立即消失 */}
|
|
|
{currentState === 'recording' && (
|
|
|
<div
|
|
|
@@ -3248,56 +3305,59 @@ export const ChatBotPage = () => {
|
|
|
);
|
|
|
})}
|
|
|
|
|
|
- {/* AnimaZone 动画形象 - 双 img 固定角色:default 层永远 default;对话层 emotion→talking,放下时静默换 default */}
|
|
|
- <div
|
|
|
- className="chatbot-anima-zone-avatar absolute"
|
|
|
- style={{
|
|
|
- width: px2vmin(238),
|
|
|
- height: px2vmin(238),
|
|
|
- top: px2vmin(292),
|
|
|
- left: '50%',
|
|
|
- transform: 'translateX(-50%)',
|
|
|
- opacity: 1,
|
|
|
- zIndex: 2,
|
|
|
- }}
|
|
|
- >
|
|
|
- <img
|
|
|
- src={defaultGifUrl || undefined}
|
|
|
- alt={currentAvatarAlt}
|
|
|
- loading="eager"
|
|
|
- decoding="async"
|
|
|
- style={{
|
|
|
- position: 'absolute',
|
|
|
- inset: 0,
|
|
|
- width: '100%',
|
|
|
- height: '100%',
|
|
|
- objectFit: 'contain',
|
|
|
- pointerEvents: 'none',
|
|
|
- zIndex: showConversationOnTop ? 0 : 1,
|
|
|
- opacity: showConversationOnTop ? 0 : 1,
|
|
|
- }}
|
|
|
- />
|
|
|
- <img
|
|
|
- src={conversationLayerSrc || undefined}
|
|
|
- alt={currentAvatarAlt}
|
|
|
- loading="eager"
|
|
|
- decoding="async"
|
|
|
+ {/* AnimaZone 动画形象 - 双 img 固定角色:default 层永远 default;对话层 emotion→talking,放下时静默换 default;
|
|
|
+ 特殊场景下未点击录音时隐藏,点击录音后显示 */}
|
|
|
+ {!(greetingContext?.scene && greetingContext.scene !== 'default' && !specialSceneAvatarDismissed) && (
|
|
|
+ <div
|
|
|
+ className="chatbot-anima-zone-avatar absolute"
|
|
|
style={{
|
|
|
- position: 'absolute',
|
|
|
- inset: 0,
|
|
|
- width: '100%',
|
|
|
- height: '100%',
|
|
|
- objectFit: 'contain',
|
|
|
- pointerEvents: 'none',
|
|
|
- zIndex: showConversationOnTop ? 1 : 0,
|
|
|
- opacity: showConversationOnTop ? 1 : 0,
|
|
|
+ width: px2vmin(238),
|
|
|
+ height: px2vmin(238),
|
|
|
+ top: px2vmin(292),
|
|
|
+ left: '50%',
|
|
|
+ transform: 'translateX(-50%)',
|
|
|
+ opacity: 1,
|
|
|
+ zIndex: 2,
|
|
|
}}
|
|
|
- />
|
|
|
- </div>
|
|
|
+ >
|
|
|
+ <img
|
|
|
+ src={defaultGifUrl || undefined}
|
|
|
+ alt={currentAvatarAlt}
|
|
|
+ loading="eager"
|
|
|
+ decoding="async"
|
|
|
+ style={{
|
|
|
+ position: 'absolute',
|
|
|
+ inset: 0,
|
|
|
+ width: '100%',
|
|
|
+ height: '100%',
|
|
|
+ objectFit: 'contain',
|
|
|
+ pointerEvents: 'none',
|
|
|
+ zIndex: showConversationOnTop ? 0 : 1,
|
|
|
+ opacity: showConversationOnTop ? 0 : 1,
|
|
|
+ }}
|
|
|
+ />
|
|
|
+ <img
|
|
|
+ src={conversationLayerSrc || undefined}
|
|
|
+ alt={currentAvatarAlt}
|
|
|
+ loading="eager"
|
|
|
+ decoding="async"
|
|
|
+ style={{
|
|
|
+ position: 'absolute',
|
|
|
+ inset: 0,
|
|
|
+ width: '100%',
|
|
|
+ height: '100%',
|
|
|
+ objectFit: 'contain',
|
|
|
+ pointerEvents: 'none',
|
|
|
+ zIndex: showConversationOnTop ? 1 : 0,
|
|
|
+ opacity: showConversationOnTop ? 1 : 0,
|
|
|
+ }}
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ )}
|
|
|
|
|
|
{/* 情景对话:字幕区域上方话题名称 + 切换话题按钮(仅特殊场景展示),与字幕左对齐;
|
|
|
- 用户在该场景该话题下开始对话后仅隐藏“切换话题按钮”,话题标签仍保留 */}
|
|
|
- {greetingContext && greetingContext.scene !== 'default' && greetingContext.topics.length > 0 && (() => {
|
|
|
+ 用户在该场景该话题下开始对话后整行隐藏(话题标签与切换按钮都不再显示) */}
|
|
|
+ {greetingContext && greetingContext.scene !== 'default' && greetingContext.topics.length > 0 && !scenarioConversationStarted && (() => {
|
|
|
const currentTopic = greetingContext.topics[currentTopicIndex];
|
|
|
const topicLabel = currentTopic
|
|
|
? (isZh ? (currentTopic.labelZh ?? currentTopic.labelEn ?? currentTopic.key) : (currentTopic.labelEn ?? currentTopic.labelZh ?? currentTopic.key))
|
|
|
@@ -3330,7 +3390,6 @@ export const ChatBotPage = () => {
|
|
|
>
|
|
|
{topicLabel}
|
|
|
</div>
|
|
|
- {!scenarioConversationStarted && (
|
|
|
<button
|
|
|
type="button"
|
|
|
aria-label="Switch topic"
|
|
|
@@ -3366,7 +3425,6 @@ export const ChatBotPage = () => {
|
|
|
draggable={false}
|
|
|
/>
|
|
|
</button>
|
|
|
- )}
|
|
|
</div>
|
|
|
);
|
|
|
})()}
|
|
|
@@ -3401,26 +3459,28 @@ export const ChatBotPage = () => {
|
|
|
</div>
|
|
|
)}
|
|
|
|
|
|
- {/* Yp 图片 - 设计稿 393×852,相对动画形象顶部 191,64×14 */}
|
|
|
- <div
|
|
|
- className="absolute"
|
|
|
- style={{
|
|
|
- top: `calc(${px2vmin(292)} + ${px2vmin(191)})`,
|
|
|
- left: '50%',
|
|
|
- transform: 'translateX(-50%)',
|
|
|
- width: px2vmin(64),
|
|
|
- height: px2vmin(14),
|
|
|
- zIndex: 10,
|
|
|
- }}
|
|
|
- >
|
|
|
- <img
|
|
|
- src={YpImage}
|
|
|
- alt={tCommon('a11y.yp')}
|
|
|
- className="w-full h-full object-contain"
|
|
|
- style={{ width: '100%', height: '100%' }}
|
|
|
- draggable={false}
|
|
|
- />
|
|
|
- </div>
|
|
|
+ {/* Yp 图片 - 设计稿 393×852,相对动画形象顶部 191,64×14;特殊场景下点击录音后再显示 */}
|
|
|
+ {!(greetingContext?.scene && greetingContext.scene !== 'default' && !specialSceneAvatarDismissed) && (
|
|
|
+ <div
|
|
|
+ className="absolute"
|
|
|
+ style={{
|
|
|
+ top: `calc(${px2vmin(292)} + ${px2vmin(191)})`,
|
|
|
+ left: '50%',
|
|
|
+ transform: 'translateX(-50%)',
|
|
|
+ width: px2vmin(64),
|
|
|
+ height: px2vmin(14),
|
|
|
+ zIndex: 10,
|
|
|
+ }}
|
|
|
+ >
|
|
|
+ <img
|
|
|
+ src={YpImage}
|
|
|
+ alt={tCommon('a11y.yp')}
|
|
|
+ className="w-full h-full object-contain"
|
|
|
+ style={{ width: '100%', height: '100%' }}
|
|
|
+ draggable={false}
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ )}
|
|
|
|
|
|
{/* 录音时按钮区域上方:0~50 秒显示 audio-wave.gif,50~60 秒显示 00:50…01:00 后自动发送 */}
|
|
|
{currentState === 'recording' && (
|