|
|
@@ -21,7 +21,6 @@ import {
|
|
|
setUserLevel,
|
|
|
getCurrentBackgroundImageUrl,
|
|
|
getCurrentBackgroundImageId,
|
|
|
- getCurrentBackgroundImageOpacity,
|
|
|
syncBackgroundImageToStorage,
|
|
|
setUserName,
|
|
|
getUserToken,
|
|
|
@@ -201,9 +200,8 @@ export const ChatBotPage = () => {
|
|
|
const isMountedRef = useRef(true);
|
|
|
const [, setUserLevelValue] = useState(() => getUserLevel());
|
|
|
const [, setHasSharedAvatarState] = useState(() => getHasSharedAvatar());
|
|
|
- // 背景图片 URL
|
|
|
- const [backgroundImageUrl, setBackgroundImageUrl] = useState<string | null>(() => {
|
|
|
- // 初始化时从 localStorage 获取背景图片 URL
|
|
|
+ // 背景图片 URL(只保留 setter,供同步逻辑使用)
|
|
|
+ const [, setBackgroundImageUrl] = useState<string | null>(() => {
|
|
|
const url = getCurrentBackgroundImageUrl();
|
|
|
return url || null;
|
|
|
});
|
|
|
@@ -1894,11 +1892,9 @@ export const ChatBotPage = () => {
|
|
|
debugLog('[AudioPlayer] onStart: cleared previous emotionGifTimer');
|
|
|
}
|
|
|
|
|
|
- // 仅在字幕开关打开时展示字幕;用户语音不会触发此回调,因此不会显示为字幕
|
|
|
+ // 仅在字幕开关打开且正在播放时展示字幕;不在此处隐藏字幕,字幕只在用户点击录音时消失
|
|
|
if (subtitleOnRef.current && appStateSnapshot.currentState === 'playing') {
|
|
|
setSubtitleVisible(true);
|
|
|
- } else {
|
|
|
- setSubtitleVisible(false);
|
|
|
}
|
|
|
|
|
|
// 用 flushSync 立即提交 GIF 状态,避免 React 批处理导致动画晚于语音 0.5~1s
|
|
|
@@ -1999,7 +1995,7 @@ export const ChatBotPage = () => {
|
|
|
flushShowTalking();
|
|
|
});
|
|
|
|
|
|
- // 音频播放结束回调:强制回归 default,本次对话收尾 + 隐藏字幕
|
|
|
+ // 音频播放结束回调:强制回归 default,本次对话收尾(字幕保留到用户下次点击录音再消失)
|
|
|
player.onEnd(() => {
|
|
|
const snapshot = useAppStore.getState();
|
|
|
const requestId = snapshot.currentRequestId;
|
|
|
@@ -2021,15 +2017,12 @@ export const ChatBotPage = () => {
|
|
|
flushSync(() => {
|
|
|
showDefaultGifRef.current?.();
|
|
|
});
|
|
|
- // 播放完成后隐藏字幕并清空字幕文本
|
|
|
- setSubtitleVisible(false);
|
|
|
- setRecognizedText('');
|
|
|
- subtitleAccumulatorRequestIdRef.current = null;
|
|
|
+ // 字幕不再在播放结束时隐藏,保留到用户下次点击录音按钮时再消失(在 handleStartRecord 中处理)
|
|
|
// 不在 onEnd 里清 responseCompleteRequestIdRef,否则 onEnd 之后才到的同轮滞后 chunk 会再次触发 onStart
|
|
|
streamStartedForRequestIdRef.current = null; // 下一轮新 request 的首包可再次触发 onStart
|
|
|
emotionGifShownForStreamRef.current = false;
|
|
|
setState('idle');
|
|
|
- reset();
|
|
|
+ reset({ keepRecognizedText: true }); // 保留字幕内容,仅在下一次点击录音时清空
|
|
|
});
|
|
|
playerRef.current = player;
|
|
|
|
|
|
@@ -2133,7 +2126,7 @@ export const ChatBotPage = () => {
|
|
|
}
|
|
|
// 恢复默认头像 GIF
|
|
|
showDefaultGif();
|
|
|
- reset();
|
|
|
+ reset({ keepRecognizedText: true });
|
|
|
setError(null);
|
|
|
}
|
|
|
return;
|
|
|
@@ -2586,6 +2579,11 @@ export const ChatBotPage = () => {
|
|
|
// 点击录音按钮时,隐藏进度条提示
|
|
|
dismissProgressBarTooltip();
|
|
|
|
|
|
+ // 用户点击录音时隐藏上一轮字幕并清空,多保留一段时间方便用户查看
|
|
|
+ setSubtitleVisible(false);
|
|
|
+ setRecognizedText('');
|
|
|
+ subtitleAccumulatorRequestIdRef.current = null;
|
|
|
+
|
|
|
// 🔥 清理播放器状态,确保上一次对话的残留状态被重置
|
|
|
playerRef.current?.stop();
|
|
|
|
|
|
@@ -2595,8 +2593,7 @@ export const ChatBotPage = () => {
|
|
|
updateRequestId(requestId);
|
|
|
setState('recording');
|
|
|
chunkIndexRef.current = 0;
|
|
|
- setRecognizedText('');
|
|
|
- subtitleAccumulatorRequestIdRef.current = null;
|
|
|
+ // 字幕已在上面点击录音时隐藏并清空
|
|
|
// 新一轮对话开始,允许本次 AI 回复播报时再播放一次情感 GIF
|
|
|
emotionGifShownForStreamRef.current = false;
|
|
|
emotionGifPlayedThisRoundRef.current = false;
|
|
|
@@ -2657,7 +2654,7 @@ export const ChatBotPage = () => {
|
|
|
// 恢复默认头像 GIF
|
|
|
showDefaultGif();
|
|
|
setState('idle');
|
|
|
- reset();
|
|
|
+ reset({ keepRecognizedText: true });
|
|
|
return;
|
|
|
}
|
|
|
};
|
|
|
@@ -2689,7 +2686,7 @@ export const ChatBotPage = () => {
|
|
|
// 恢复默认头像 GIF
|
|
|
showDefaultGif();
|
|
|
performanceLogger.clearRequest(currentRequestId, 'websocket_unready_before_send');
|
|
|
- reset();
|
|
|
+ reset({ keepRecognizedText: true });
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
@@ -2712,7 +2709,7 @@ export const ChatBotPage = () => {
|
|
|
// 恢复默认头像 GIF
|
|
|
showDefaultGif();
|
|
|
performanceLogger.clearRequest(currentRequestId, 'websocket_disconnected_before_send');
|
|
|
- reset();
|
|
|
+ reset({ keepRecognizedText: true });
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
@@ -2762,7 +2759,7 @@ export const ChatBotPage = () => {
|
|
|
// 恢复默认头像 GIF
|
|
|
showDefaultGif();
|
|
|
setState('idle');
|
|
|
- reset();
|
|
|
+ reset({ keepRecognizedText: true });
|
|
|
performanceLogger.clearRequest(currentRequestId, '响应超时');
|
|
|
}
|
|
|
}, 10000);
|
|
|
@@ -2792,7 +2789,7 @@ export const ChatBotPage = () => {
|
|
|
wsRef.current?.sendCancel(currentRequestId);
|
|
|
cancelledRequestIdsRef.current.add(currentRequestId);
|
|
|
performanceLogger.clearRequest(currentRequestId);
|
|
|
- reset();
|
|
|
+ reset({ keepRecognizedText: true });
|
|
|
};
|
|
|
|
|
|
const currentAvatarAlt = currentState === 'playing' ? 'AnimaZone Avatar Animated' : 'AnimaZone Avatar';
|
|
|
@@ -2821,23 +2818,15 @@ export const ChatBotPage = () => {
|
|
|
[spawnStone, fetchConversationStoneCount]
|
|
|
);
|
|
|
|
|
|
- // 根据背景图片获取对应的树木装饰图片URL(默认背景下录音时切到 bg-chatting.png,发送/取消后恢复)
|
|
|
+ // 根据背景图片获取对应的树木装饰图片URL(/chatbot 界面统一使用默认图片,不随用户选择的新年等背景切换)
|
|
|
const getTreesImage = (): string | null => {
|
|
|
- const bgId = getCurrentBackgroundImageId();
|
|
|
- const isDefaultBg = !bgId || bgId === 'default';
|
|
|
- if (isDefaultBg && currentState === 'recording') {
|
|
|
+ if (currentState === 'recording') {
|
|
|
return bgChattingUrl;
|
|
|
}
|
|
|
- // 实时从 localStorage 读取最新的背景图片URL,使用背景图片作为树木装饰
|
|
|
- const currentBgUrl = getCurrentBackgroundImageUrl() || backgroundImageUrl;
|
|
|
- if (currentBgUrl && currentBgUrl.includes('init.svg')) {
|
|
|
- // 如果是旧的 init.svg,强制替换为 init1.svg
|
|
|
- return getOssUrl('init1.svg');
|
|
|
- }
|
|
|
- return currentBgUrl;
|
|
|
+ return getOssUrl('init1.svg');
|
|
|
};
|
|
|
|
|
|
- // 根据背景图片ID获取树木装饰图片的样式配置
|
|
|
+ // 根据背景图片ID获取树木装饰图片的样式配置(/chatbot 使用默认背景,透明度固定为 1)
|
|
|
const getTreesImageStyle = (): React.CSSProperties => {
|
|
|
const baseStyle: React.CSSProperties = {
|
|
|
width: '100%',
|
|
|
@@ -2846,11 +2835,9 @@ export const ChatBotPage = () => {
|
|
|
left: 0,
|
|
|
objectFit: 'cover',
|
|
|
zIndex: 1,
|
|
|
+ opacity: 1,
|
|
|
};
|
|
|
-
|
|
|
- // 从 localStorage 读取透明度配置(从数据库metadata中获取)
|
|
|
- const opacity = getCurrentBackgroundImageOpacity();
|
|
|
- return { ...baseStyle, opacity };
|
|
|
+ return baseStyle;
|
|
|
};
|
|
|
|
|
|
const defaultGifUrl = avatarGifSet?.default ?? '';
|
|
|
@@ -3219,7 +3206,7 @@ export const ChatBotPage = () => {
|
|
|
</div>
|
|
|
|
|
|
{/* 字幕区域 - 设计稿 393×852:top 500,高 92,宽 312,左右居中 */}
|
|
|
- {subtitleOn && subtitleVisible && recognizedText && currentState === 'playing' && (
|
|
|
+ {subtitleOn && subtitleVisible && recognizedText && (
|
|
|
<div
|
|
|
ref={subtitleContainerRef}
|
|
|
className="chatbot-subtitle-container"
|