);
}
-export default ToolBar;
+export default memo(ToolBar);
diff --git a/src/pages/MainPage/MainArea/Room/index.module.less b/src/pages/MainPage/MainArea/Room/index.module.less
index e5978ed..03d4d62 100644
--- a/src/pages/MainPage/MainArea/Room/index.module.less
+++ b/src/pages/MainPage/MainArea/Room/index.module.less
@@ -172,6 +172,15 @@
line-height: 22px;
}
+.closed {
+ width: 100%;
+ text-align: center;
+ color: #737A87;
+ font-size: 14px;
+ font-weight: 400;
+ line-height: 19.6px;
+}
+
.btns {
width: 100%;
display: flex;
@@ -262,6 +271,10 @@
border-radius: 8px;
}
+ .camera-player-hidden {
+ display: none !important;
+ }
+
.camera-placeholder {
width: 100%;
display: flex;
@@ -273,6 +286,8 @@
.camera-placeholder-close-note {
margin-bottom: 8px;
+ width: 60px;
+ height: 60px;
}
.camera-open-btn {
diff --git a/src/pages/MainPage/Menu/components/AISettingAnchor/index.module.less b/src/pages/MainPage/Menu/components/AISettingAnchor/index.module.less
new file mode 100644
index 0000000..1ede302
--- /dev/null
+++ b/src/pages/MainPage/Menu/components/AISettingAnchor/index.module.less
@@ -0,0 +1,44 @@
+/**
+ * Copyright 2025 Beijing Volcano Engine Technology Co., Ltd. All Rights Reserved.
+ * SPDX-license-identifier: BSD-3-Clause
+ */
+
+.row {
+ width: 100%;
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ cursor: pointer;
+
+ .firstPart {
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ width: 90%;
+ color: var(--text-color-text-2, var(--text-color-text-2, #42464E));
+ text-align: center;
+
+ font-family: "PingFang SC";
+ font-size: 13px;
+ font-style: normal;
+ font-weight: 500;
+ line-height: 22px;
+ letter-spacing: 0.039px;
+ }
+
+ .finalPart {
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ width: 10%;
+ justify-content: flex-end;
+
+ .rightOutlined {
+ font-size: 12px;
+ }
+ }
+
+ .icon {
+ margin-right: 4px;
+ }
+}
\ No newline at end of file
diff --git a/src/pages/MainPage/Menu/components/AISettingAnchor/index.tsx b/src/pages/MainPage/Menu/components/AISettingAnchor/index.tsx
new file mode 100644
index 0000000..d3cbaf2
--- /dev/null
+++ b/src/pages/MainPage/Menu/components/AISettingAnchor/index.tsx
@@ -0,0 +1,29 @@
+/**
+ * Copyright 2025 Beijing Volcano Engine Technology Co., Ltd. All Rights Reserved.
+ * SPDX-license-identifier: BSD-3-Clause
+ */
+
+import { useState } from 'react';
+import { IconRight } from '@arco-design/web-react/icon';
+import AISettings from '@/components/AISettings';
+import styles from './index.module.less';
+
+function AISettingAnchor() {
+ const [open, setOpen] = useState(false);
+
+ const handleOpenDrawer = () => setOpen(true);
+ const handleCloseDrawer = () => setOpen(false);
+ return (
+ <>
+
+ >
+ );
+}
+
+export default AISettingAnchor;
diff --git a/src/pages/MainPage/Menu/components/Operation/index.tsx b/src/pages/MainPage/Menu/components/Operation/index.tsx
index 860944e..7f5c453 100644
--- a/src/pages/MainPage/Menu/components/Operation/index.tsx
+++ b/src/pages/MainPage/Menu/components/Operation/index.tsx
@@ -6,16 +6,18 @@
import { MediaType } from '@volcengine/rtc';
import DeviceDrawerButton from '../DeviceDrawerButton';
import { useVisionMode } from '@/lib/useCommon';
+import AISettingAnchor from '../AISettingAnchor';
import Interrupt from '../Interrupt';
import styles from './index.module.less';
function Operation() {
- const isVisionMode = useVisionMode();
+ const { isVisionMode, isScreenMode } = useVisionMode();
return (
);
}
diff --git a/src/pages/MainPage/Menu/index.tsx b/src/pages/MainPage/Menu/index.tsx
index a60a6ad..1542751 100644
--- a/src/pages/MainPage/Menu/index.tsx
+++ b/src/pages/MainPage/Menu/index.tsx
@@ -4,6 +4,7 @@
*/
import VERTC from '@volcengine/rtc';
+import { useEffect, useState } from 'react';
import { Tooltip, Typography } from '@arco-design/web-react';
import { useDispatch, useSelector } from 'react-redux';
import { useVisionMode } from '@/lib/useCommon';
@@ -13,37 +14,39 @@ import Operation from './components/Operation';
import { Questions } from '@/config';
import { COMMAND, INTERRUPT_PRIORITY } from '@/utils/handler';
import CameraArea from '../MainArea/Room/CameraArea';
-import { setCurrentMsg, setHistoryMsg } from '@/store/slices/room';
+import { setHistoryMsg, setInterruptMsg } from '@/store/slices/room';
import utils from '@/utils/utils';
+import packageJson from '../../../../package.json';
import styles from './index.module.less';
function Menu() {
const dispatch = useDispatch();
+ const [question, setQuestion] = useState('');
const room = useSelector((state: RootState) => state.room);
const scene = room.scene;
const isJoined = room?.isJoined;
const isVisionMode = useVisionMode();
- const handleQuestion = (question: string) => {
- RtcClient.commandAudioBot(COMMAND.EXTERNAL_TEXT_TO_LLM, INTERRUPT_PRIORITY.HIGH, question);
- dispatch(
- setHistoryMsg({
- text: question,
- user: RtcClient.basicInfo.user_id,
- paragraph: true,
- definite: true,
- })
- );
- dispatch(
- setCurrentMsg({
- text: question,
- user: RtcClient.basicInfo.user_id,
- paragraph: true,
- definite: true,
- })
- );
+ const handleQuestion = (que: string) => {
+ RtcClient.commandAudioBot(COMMAND.EXTERNAL_TEXT_TO_LLM, INTERRUPT_PRIORITY.HIGH, que);
+ setQuestion(que);
};
+ useEffect(() => {
+ if (question && !room.isAITalking) {
+ dispatch(setInterruptMsg());
+ dispatch(
+ setHistoryMsg({
+ text: question,
+ user: RtcClient.basicInfo.user_id,
+ paragraph: true,
+ definite: true,
+ })
+ );
+ setQuestion('');
+ }
+ }, [question, room.isAITalking]);
+
return (
{isJoined && utils.isMobile() && isVisionMode ? (
@@ -52,7 +55,7 @@ function Menu() {
-
Demo Version 1.4.0
+
Demo Version {packageJson.version}
SDK Version {VERTC.getSdkVersion()}
{isJoined ? (
diff --git a/src/store/slices/room.ts b/src/store/slices/room.ts
index 0812fe7..89e6af4 100644
--- a/src/store/slices/room.ts
+++ b/src/store/slices/room.ts
@@ -11,7 +11,6 @@ import {
RemoteAudioStats,
} from '@volcengine/rtc';
import config, { SCENE } from '@/config';
-import utils from '@/utils/utils';
export interface IUser {
username?: string;
@@ -33,6 +32,7 @@ export interface Msg {
time: string;
user: string;
paragraph?: boolean;
+ definite?: boolean;
isInterrupted?: boolean;
}
@@ -59,6 +59,10 @@ export interface RoomState {
* @brief AI 是否正在说话
*/
isAITalking: boolean;
+ /**
+ * @brief AI 思考中
+ */
+ isAIThinking: boolean;
/**
* @brief 用户是否正在说话
*/
@@ -99,12 +103,14 @@ const initialState: RoomState = {
scene: SCENE.INTELLIGENT_ASSISTANT,
remoteUsers: [],
localUser: {
- publishAudio: true,
- publishVideo: true,
+ publishAudio: false,
+ publishVideo: false,
+ publishScreen: false,
},
autoPlayFailUser: [],
isJoined: false,
isAIGCEnable: false,
+ isAIThinking: false,
isAITalking: false,
isUserTalking: false,
networkQuality: NetworkQuality.UNKNOWN,
@@ -131,15 +137,19 @@ export const roomSlice = createSlice({
}
) => {
state.roomId = payload.roomId;
- state.localUser = payload.user;
+ state.localUser = {
+ ...state.localUser,
+ ...payload.user,
+ };
state.isJoined = true;
},
localLeaveRoom: (state) => {
state.roomId = undefined;
state.time = -1;
state.localUser = {
- publishAudio: true,
- publishVideo: true,
+ publishAudio: false,
+ publishVideo: false,
+ publishScreen: false,
};
state.remoteUsers = [];
state.isJoined = false;
@@ -159,7 +169,7 @@ export const roomSlice = createSlice({
updateLocalUser: (state, { payload }: { payload: Partial }) => {
state.localUser = {
...state.localUser,
- ...payload,
+ ...(payload || {}),
};
},
@@ -204,8 +214,14 @@ export const roomSlice = createSlice({
state.isAIGCEnable = payload.isAIGCEnable;
},
updateAITalkState: (state, { payload }) => {
+ state.isAIThinking = false;
+ state.isUserTalking = false;
state.isAITalking = payload.isAITalking;
},
+ updateAIThinkState: (state, { payload }) => {
+ state.isAIThinking = payload.isAIThinking;
+ state.isUserTalking = false;
+ },
updateAIConfig: (state, { payload }) => {
state.aiConfig = Object.assign(state.aiConfig, payload);
},
@@ -213,36 +229,74 @@ export const roomSlice = createSlice({
state.msgHistory = [];
},
setHistoryMsg: (state, { payload }) => {
- const paragraph = payload.paragraph;
- const aiTalking = payload.user === config.BotName;
- const userTalking = payload.user === state.localUser.userId;
- if (paragraph) {
- if (state.isAITalking) {
- state.isAITalking = false;
- }
- if (state.isUserTalking) {
- state.isUserTalking = false;
+ const { paragraph, definite } = payload;
+ /** 是否需要再创建新句子 */
+ const shouldCreateSentence = payload.definite;
+ state.isUserTalking = payload.user === state.localUser.userId;
+ if (state.msgHistory.length) {
+ const lastMsg = state.msgHistory.at(-1)!;
+ /** 当前讲话人更新字幕 */
+ if (lastMsg.user === payload.user) {
+ /** 如果上一句话是完整的 & 本次的话也是完整的, 则直接塞入 */
+ if (lastMsg.definite) {
+ state.msgHistory.push({
+ value: payload.text,
+ time: new Date().toString(),
+ user: payload.user,
+ definite,
+ paragraph,
+ });
+ } else {
+ /** 话未说完, 更新文字内容 */
+ lastMsg.value = payload.text;
+ lastMsg.time = new Date().toString();
+ lastMsg.paragraph = paragraph;
+ lastMsg.definite = definite;
+ lastMsg.user = payload.user;
+ }
+ /** 如果本次的话已经说完了, 提前塞入空字符串做准备 */
+ if (shouldCreateSentence) {
+ state.msgHistory.push({
+ value: '',
+ time: new Date().toString(),
+ user: '',
+ });
+ }
+ } else {
+ /** 换人说话了,塞入新句子 */
+ state.msgHistory.push({
+ value: payload.text,
+ time: new Date().toString(),
+ user: payload.user,
+ definite,
+ paragraph,
+ });
}
} else {
- if (state.isAITalking !== aiTalking) {
- state.isAITalking = aiTalking;
- }
- if (state.isUserTalking !== userTalking) {
- state.isUserTalking = userTalking;
- }
+ /** 首句话首字不会被打断 */
+ state.msgHistory.push({
+ value: payload.text,
+ time: new Date().toString(),
+ user: payload.user,
+ paragraph,
+ });
}
- utils.addMsgWithoutDuplicate(state.msgHistory, {
- user: payload.user,
- value: payload.text,
- time: new Date().toLocaleString(),
- isInterrupted: false,
- paragraph,
- });
},
setInterruptMsg: (state) => {
- const msg = state.msgHistory[state.msgHistory.length - 1];
- msg.isInterrupted = true;
- state.msgHistory[state.msgHistory.length - 1] = msg;
+ state.isAITalking = false;
+ if (!state.msgHistory.length) {
+ return;
+ }
+ /** 找到最后一个末尾的字幕, 将其状态置换为打断 */
+ for (let id = state.msgHistory.length - 1; id >= 0; id--) {
+ const msg = state.msgHistory[id];
+ if (msg.value) {
+ if (!msg.definite) {
+ state.msgHistory[id].isInterrupted = true;
+ }
+ break;
+ }
+ }
},
clearCurrentMsg: (state) => {
state.currentConversation = {};
@@ -250,10 +304,6 @@ export const roomSlice = createSlice({
state.isAITalking = false;
state.isUserTalking = false;
},
- setCurrentMsg: (state, { payload }) => {
- const { user, ...info } = payload;
- state.currentConversation[user || state.localUser.userId] = info;
- },
},
});
@@ -270,9 +320,9 @@ export const {
clearAutoPlayFail,
updateAIGCState,
updateAITalkState,
+ updateAIThinkState,
updateAIConfig,
setHistoryMsg,
- setCurrentMsg,
clearHistoryMsg,
clearCurrentMsg,
setInterruptMsg,
diff --git a/src/utils/handler.ts b/src/utils/handler.ts
index ef7fc67..b232bf1 100644
--- a/src/utils/handler.ts
+++ b/src/utils/handler.ts
@@ -6,10 +6,10 @@
import { useDispatch } from 'react-redux';
import logger from './logger';
import {
- setCurrentMsg,
setHistoryMsg,
setInterruptMsg,
updateAITalkState,
+ updateAIThinkState,
} from '@/store/slices/room';
import RtcClient from '@/lib/RtcClient';
import Utils from '@/utils/utils';
@@ -89,11 +89,16 @@ export const useMessageHandler = () => {
const { Code, Description } = Stage || {};
logger.debug(Code, Description);
switch (Code) {
+ case AGENT_BRIEF.THINKING:
+ dispatch(updateAIThinkState({ isAIThinking: true }));
+ break;
+ case AGENT_BRIEF.SPEAKING:
+ dispatch(updateAITalkState({ isAITalking: true }));
+ break;
case AGENT_BRIEF.FINISHED:
dispatch(updateAITalkState({ isAITalking: false }));
break;
case AGENT_BRIEF.INTERRUPTED:
- dispatch(updateAITalkState({ isAITalking: false }));
dispatch(setInterruptMsg());
break;
default:
@@ -118,7 +123,6 @@ export const useMessageHandler = () => {
dispatch(setHistoryMsg({ text: msg, user, paragraph, definite }));
}
}
- dispatch(setCurrentMsg({ msg, definite, user, paragraph }));
}
},
/**
@@ -141,8 +145,8 @@ export const useMessageHandler = () => {
ToolCallID: parsed?.tool_calls?.[0]?.id,
Content: map[name.toLocaleLowerCase().replaceAll('_', '')],
}),
- 'func',
- ),
+ 'func'
+ )
);
},
};
diff --git a/src/utils/utils.ts b/src/utils/utils.ts
index 2166d39..ffdd964 100644
--- a/src/utils/utils.ts
+++ b/src/utils/utils.ts
@@ -3,12 +3,7 @@
* SPDX-license-identifier: BSD-3-Clause
*/
-import { Msg, RoomState } from '@/store/slices/room';
-import RtcClient from '@/lib/RtcClient';
-
-
class Utils {
-
formatTime = (time: number): string => {
if (time < 0) {
return '00:00';
@@ -46,9 +41,9 @@ class Utils {
const query = window.location.search.substring(1);
const pairs = query.split('&');
return pairs.reduce<{ [key: string]: string }>((queries, pair) => {
- const [key, value] = decodeURIComponent(pair).split('=');
+ const [key, value] = pair.split('=');
if (key && value) {
- queries[key] = value;
+ queries[key] = decodeURIComponent(value);
}
return queries;
}, {});
@@ -58,34 +53,6 @@ class Utils {
isArray = Array.isArray;
- debounce = (func: (...args: any[]) => void, wait: number) => {
- let timeoutId: ReturnType | null = null;
- return function (...args: any[]) {
- if (timeoutId !== null) {
- clearTimeout(timeoutId);
- }
-
- timeoutId = setTimeout(() => {
- func(...args);
- }, wait);
- };
- };
-
- addMsgWithoutDuplicate = (arr: RoomState['msgHistory'], added: Msg) => {
- if (arr.length) {
- const last = arr.at(-1)!;
- const { user, value, isInterrupted } = last;
- if (
- (added.user === RtcClient.basicInfo.user_id && last.user === added.user) ||
- (user === added.user && added.value.startsWith(value) && value.trim())
- ) {
- arr.pop();
- added.isInterrupted = isInterrupted;
- }
- }
- arr.push(added);
- };
-
/**
* @brief 将字符串包装成 TLV
*/
@@ -119,7 +86,7 @@ class Utils {
* @note TLV 数据格式
* | magic number | length(big-endian) | value |
* @param {ArrayBufferLike} tlvBuffer
- * @returns
+ * @returns
*/
tlv2String(tlvBuffer: ArrayBufferLike) {
const typeBuffer = new Uint8Array(tlvBuffer, 0, 4);
diff --git a/yarn.lock b/yarn.lock
index 7d8dc66..2f1e555 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2210,10 +2210,10 @@
"@typescript-eslint/types" "5.31.0"
eslint-visitor-keys "^3.3.0"
-"@volcengine/rtc@4.58.9":
- version "4.58.9"
- resolved "https://registry.yarnpkg.com/@volcengine/rtc/-/rtc-4.58.9.tgz#841ebaddd5d4963c71abd33037bd76d1d490d928"
- integrity sha512-nnXnNW9pVo8ynBSxVe0ikNIdxWfoSx5oOnwK7EoMCXdc2bJgHATpz/B+Kv2F1k4GjYAbo7ZcOm/g3cchvHgH5Q==
+"@volcengine/rtc@4.66.1":
+ version "4.66.1"
+ resolved "https://registry.yarnpkg.com/@volcengine/rtc/-/rtc-4.66.1.tgz#1934c269b31216f43718ae46b169c59ac5e474f2"
+ integrity sha512-APznH6eosmKJC1HYJJ8s6G3Mq3OSgw6ivv6uCiayM5QNMBj+GW6zxf+MVsk5rm6r4R92TLwQErWonJ8yzGO4xA==
dependencies:
eventemitter3 "^4.0.7"