|
|
@@ -9,6 +9,7 @@ import { prepareMobileAudio, requestVolumeControl, isMobileDevice } from '@/util
|
|
|
import { setStorage, getStorage, STORAGE_KEYS, markRouteVisited, getUserId } from '@/utils/storage';
|
|
|
import { onScreenResize, calculateLayout, type CalculatedLayout, updateTooSmallFlag } from '@/utils/screenSize';
|
|
|
import { performanceLogger } from '@/utils/logger';
|
|
|
+import { getUserInfo } from '@/utils/api';
|
|
|
import type { Message } from '@/types';
|
|
|
import '@/styles/chatbot-fix.css';
|
|
|
import avatarC from '@/assets/Avatar-C.svg';
|
|
|
@@ -34,13 +35,15 @@ export const ChatBotPage = () => {
|
|
|
currentRequestId,
|
|
|
currentEmotion,
|
|
|
conversationProgress,
|
|
|
+ progressLoopCount,
|
|
|
setState,
|
|
|
setRequestId,
|
|
|
setEmotion,
|
|
|
setRecognizedText,
|
|
|
setError,
|
|
|
setConnected,
|
|
|
- incrementProgress,
|
|
|
+ updateProgressByDuration,
|
|
|
+ setProgress,
|
|
|
reset,
|
|
|
} = useAppStore();
|
|
|
|
|
|
@@ -73,6 +76,8 @@ export const ChatBotPage = () => {
|
|
|
// 用于动画的显示进度(平滑过渡)
|
|
|
const [displayProgress, setDisplayProgress] = useState(0);
|
|
|
const animationFrameRef = useRef<number>();
|
|
|
+ const totalProgressRef = useRef(0);
|
|
|
+ const shouldSkipProgressAnimationRef = useRef(true);
|
|
|
|
|
|
// V2: 完全计算式布局系统
|
|
|
const [layout, setLayout] = useState<CalculatedLayout>(() => {
|
|
|
@@ -87,86 +92,66 @@ export const ChatBotPage = () => {
|
|
|
emotionReceived: 0,
|
|
|
});
|
|
|
|
|
|
- // 进度动画效果
|
|
|
+ // 进度动画效果(支持多圈连续动画)
|
|
|
useEffect(() => {
|
|
|
- const targetProgress = conversationProgress;
|
|
|
- const startProgress = displayProgress;
|
|
|
- const diff = targetProgress - startProgress;
|
|
|
-
|
|
|
- if (Math.abs(diff) < 0.1) {
|
|
|
- setDisplayProgress(targetProgress);
|
|
|
+ const targetTotal = progressLoopCount * 100 + conversationProgress;
|
|
|
+ const startTotal = totalProgressRef.current;
|
|
|
+ const diff = targetTotal - startTotal;
|
|
|
+
|
|
|
+ if (shouldSkipProgressAnimationRef.current) {
|
|
|
+ shouldSkipProgressAnimationRef.current = false;
|
|
|
+ totalProgressRef.current = targetTotal;
|
|
|
+ const normalized = ((targetTotal % 100) + 100) % 100;
|
|
|
+ setDisplayProgress(normalized);
|
|
|
return;
|
|
|
}
|
|
|
-
|
|
|
- // 检测循环重置:从 100% 跳到 10% 的情况
|
|
|
- const isLoopReset = startProgress >= 100 && targetProgress < startProgress;
|
|
|
-
|
|
|
- // 如果检测到循环重置,先重置为 0,然后从 0 动画到目标值
|
|
|
- if (isLoopReset) {
|
|
|
- // 取消之前的动画
|
|
|
- if (animationFrameRef.current) {
|
|
|
- cancelAnimationFrame(animationFrameRef.current);
|
|
|
- }
|
|
|
-
|
|
|
- // 先重置为 0,使用双重 requestAnimationFrame 确保重置完成
|
|
|
- setDisplayProgress(0);
|
|
|
- requestAnimationFrame(() => {
|
|
|
- requestAnimationFrame(() => {
|
|
|
- const duration = 600; // 600ms 动画时长
|
|
|
- const startTime = performance.now();
|
|
|
-
|
|
|
- const animate = (currentTime: number) => {
|
|
|
- const elapsed = currentTime - startTime;
|
|
|
- const progress = Math.min(elapsed / duration, 1);
|
|
|
-
|
|
|
- // 使用 easeOutCubic 缓动函数
|
|
|
- const easeProgress = 1 - Math.pow(1 - progress, 3);
|
|
|
- const currentValue = 0 + targetProgress * easeProgress;
|
|
|
-
|
|
|
- setDisplayProgress(currentValue);
|
|
|
-
|
|
|
- if (progress < 1) {
|
|
|
- animationFrameRef.current = requestAnimationFrame(animate);
|
|
|
- } else {
|
|
|
- setDisplayProgress(targetProgress);
|
|
|
- }
|
|
|
- };
|
|
|
-
|
|
|
- animationFrameRef.current = requestAnimationFrame(animate);
|
|
|
- });
|
|
|
- });
|
|
|
+
|
|
|
+ // 若变化极小,直接同步
|
|
|
+ if (Math.abs(diff) < 0.1) {
|
|
|
+ totalProgressRef.current = targetTotal;
|
|
|
+ const normalized = ((targetTotal % 100) + 100) % 100;
|
|
|
+ setDisplayProgress(normalized);
|
|
|
return;
|
|
|
}
|
|
|
-
|
|
|
- // 正常增长动画
|
|
|
- const duration = 600; // 600ms 动画时长
|
|
|
+
|
|
|
+ // 每 1% 的进度约 8ms,限制在 0.4s~2.4s 之间
|
|
|
+ const durationPerPercent = 8;
|
|
|
+ const duration = Math.max(400, Math.min(2400, Math.abs(diff) * durationPerPercent));
|
|
|
const startTime = performance.now();
|
|
|
-
|
|
|
+
|
|
|
+ if (animationFrameRef.current) {
|
|
|
+ cancelAnimationFrame(animationFrameRef.current);
|
|
|
+ }
|
|
|
+
|
|
|
const animate = (currentTime: number) => {
|
|
|
const elapsed = currentTime - startTime;
|
|
|
const progress = Math.min(elapsed / duration, 1);
|
|
|
-
|
|
|
- // 使用 easeOutCubic 缓动函数
|
|
|
+
|
|
|
+ // easeOutCubic 缓动
|
|
|
const easeProgress = 1 - Math.pow(1 - progress, 3);
|
|
|
- const currentValue = startProgress + diff * easeProgress;
|
|
|
-
|
|
|
- setDisplayProgress(currentValue);
|
|
|
-
|
|
|
+ const currentTotal = startTotal + diff * easeProgress;
|
|
|
+ totalProgressRef.current = currentTotal;
|
|
|
+
|
|
|
+ const normalized = ((currentTotal % 100) + 100) % 100;
|
|
|
+ setDisplayProgress(normalized);
|
|
|
+
|
|
|
if (progress < 1) {
|
|
|
animationFrameRef.current = requestAnimationFrame(animate);
|
|
|
} else {
|
|
|
- setDisplayProgress(targetProgress);
|
|
|
+ totalProgressRef.current = targetTotal;
|
|
|
+ const finalNormalized = ((targetTotal % 100) + 100) % 100;
|
|
|
+ setDisplayProgress(finalNormalized);
|
|
|
}
|
|
|
};
|
|
|
-
|
|
|
+
|
|
|
animationFrameRef.current = requestAnimationFrame(animate);
|
|
|
-
|
|
|
+
|
|
|
return () => {
|
|
|
if (animationFrameRef.current) {
|
|
|
cancelAnimationFrame(animationFrameRef.current);
|
|
|
}
|
|
|
};
|
|
|
- }, [conversationProgress]);
|
|
|
+ }, [conversationProgress, progressLoopCount]);
|
|
|
|
|
|
// 控制声纹容器高度变化
|
|
|
useEffect(() => {
|
|
|
@@ -298,6 +283,27 @@ export const ChatBotPage = () => {
|
|
|
|
|
|
const init = async () => {
|
|
|
try {
|
|
|
+ // 获取用户信息并初始化进度条
|
|
|
+ const user_id = getUserId();
|
|
|
+ if (user_id) {
|
|
|
+ try {
|
|
|
+ const userInfo = await getUserInfo(user_id);
|
|
|
+ if (userInfo.success && userInfo.data.total_speech_duration_ms) {
|
|
|
+ // 根据总说话时长计算初始进度(30秒=100%)
|
|
|
+ const PROGRESS_FULL_DURATION_MS = 30 * 1000; // 30000ms
|
|
|
+ const totalDurationMs = userInfo.data.total_speech_duration_ms || 0;
|
|
|
+ const totalProgressPercent = (totalDurationMs / PROGRESS_FULL_DURATION_MS) * 100;
|
|
|
+ const initialLoopCount = Math.floor(totalProgressPercent / 100);
|
|
|
+ const initialProgress = totalProgressPercent % 100;
|
|
|
+ shouldSkipProgressAnimationRef.current = true;
|
|
|
+ setProgress(initialProgress, initialLoopCount);
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ // 获取用户信息失败不影响主流程,静默处理
|
|
|
+ console.warn('获取用户信息失败:', error);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
// 初始化录音器
|
|
|
const recorder = new AudioRecorder();
|
|
|
await recorder.initialize();
|
|
|
@@ -311,7 +317,7 @@ export const ChatBotPage = () => {
|
|
|
if (requestId) {
|
|
|
performanceLogger.recordPlaybackEnd(requestId);
|
|
|
}
|
|
|
- incrementProgress(); // AI说话完成后增加进度
|
|
|
+ // 进度条已基于用户说话时长更新,无需在播放完成后再次更新
|
|
|
setState('idle');
|
|
|
reset();
|
|
|
});
|
|
|
@@ -319,7 +325,6 @@ export const ChatBotPage = () => {
|
|
|
|
|
|
// 初始化WebSocket(添加user_id查询参数)
|
|
|
const ws = new WebSocketManager();
|
|
|
- const user_id = getUserId();
|
|
|
const wsUrl = user_id ? `${WS_URL}?user_id=${encodeURIComponent(user_id)}` : WS_URL;
|
|
|
ws.connect(wsUrl, (connected) => {
|
|
|
setConnected(connected);
|
|
|
@@ -502,9 +507,18 @@ export const ChatBotPage = () => {
|
|
|
sendTimeRef.current = Date.now();
|
|
|
performanceLogger.recordEnd(currentRequestId, chunkIndexRef.current);
|
|
|
|
|
|
+ // 计算说话时长(毫秒)
|
|
|
+ const speechDuration = timingRef.current.recordEnd - timingRef.current.recordStart;
|
|
|
+
|
|
|
+ // 更新进度条(基于说话时长,30秒=100%)
|
|
|
+ if (speechDuration > 0) {
|
|
|
+ updateProgressByDuration(speechDuration);
|
|
|
+ }
|
|
|
+
|
|
|
recorderRef.current?.stop();
|
|
|
|
|
|
- wsRef.current?.sendAudioEnd(currentRequestId, chunkIndexRef.current);
|
|
|
+ // 发送音频结束消息,包含说话时长信息
|
|
|
+ wsRef.current?.sendAudioEnd(currentRequestId, chunkIndexRef.current, speechDuration);
|
|
|
setState('sending');
|
|
|
|
|
|
// 设置超时保护:10秒后如果还在 sending 或 playing 状态,自动重置
|