589 lines
19 KiB
JavaScript
589 lines
19 KiB
JavaScript
/**
|
|
* 华为交互数字人 - HwICSUiSdk 集成示例
|
|
*/
|
|
|
|
import { HwICSUiSdk } from '../hwICSUiSdk/HwICSUiSdk.esm.js';
|
|
import { fetchOnceCode } from './api.js';
|
|
|
|
const ICSEventKey = {
|
|
ERROR: 'error',
|
|
JOB_INFO_CHANGE: 'jobInfoChange',
|
|
SPEAKING_START: 'speakingStart',
|
|
SPEAKING_STOP: 'speakingStop',
|
|
SPEECH_RECOGNIZED: 'speechRecognized',
|
|
SEMANTIC_RECOGNIZED: 'semanticRecognized',
|
|
ENTER_SLEEP: 'enterSleep',
|
|
ENTER_ACTIVE: 'enterActive',
|
|
LANGUAGE_INFO_CHANGE: 'languageInfoChange',
|
|
JOB_END_MENTION: 'jobEndMention',
|
|
JOB_END: 'jobEnd'
|
|
};
|
|
|
|
const InteractionMode = {
|
|
AUDIO: 'AUDIO',
|
|
TEXT: 'TEXT'
|
|
};
|
|
|
|
const ConfigKey = {
|
|
ENABLE_CAPTION: 'enableCaption',
|
|
ENABLE_CHAT_BTN: 'enableChatBtn',
|
|
ENABLE_HOT_ISSUES: 'enableHotIssues',
|
|
ENABLE_WEAK_ERROR_INFO: 'enableWeakErrorInfo',
|
|
SHOW_LANGUAGE_UI: 'showLanguageUI',
|
|
ENABLE_DEVICE_SETTING: 'enableDeviceSetting',
|
|
ENABLE_NETWORK_CHECK: 'enableNetworkCheck',
|
|
AUTO_PLAY_REMOTE_VIDEO: 'autoPlayRemoteVideo',
|
|
DEFAULT_MUTE_REMOTE_AUDIO: 'defaultMuteRemoteAudio'
|
|
};
|
|
|
|
const state = {
|
|
isInitialized: false,
|
|
isChatting: false,
|
|
isSpeaking: false,
|
|
isMuted: false,
|
|
jobInfo: null,
|
|
currentInteractionMode: InteractionMode.AUDIO
|
|
};
|
|
|
|
const elements = {
|
|
serverAddress: document.getElementById('serverAddress'),
|
|
robotId: document.getElementById('robotId'),
|
|
onceCode: document.getElementById('onceCode'),
|
|
|
|
initBtn: document.getElementById('initBtn'),
|
|
destroyBtn: document.getElementById('destroyBtn'),
|
|
getOnceCodeBtn: document.getElementById('getOnceCodeBtn'),
|
|
clearLogBtn: document.getElementById('clearLogBtn'),
|
|
fullscreenBtn: document.getElementById('fullscreenBtn'),
|
|
toggleConfigBtn: document.getElementById('toggleConfigBtn'),
|
|
toggleCaptionBtn: document.getElementById('toggleCaptionBtn'),
|
|
toggleLogBtn: document.getElementById('toggleLogBtn'),
|
|
applyCaptionStyleBtn: document.getElementById('applyCaptionStyleBtn'),
|
|
|
|
captionWidth: document.getElementById('captionWidth'),
|
|
captionHeight: document.getElementById('captionHeight'),
|
|
captionFontSize: document.getElementById('captionFontSize'),
|
|
captionRight: document.getElementById('captionRight'),
|
|
captionBottom: document.getElementById('captionBottom'),
|
|
|
|
logLevel: document.getElementById('logLevel'),
|
|
|
|
digitalHumanBox: document.getElementById('digitalHumanBox'),
|
|
logContainer: document.getElementById('logContainer'),
|
|
connectionStatus: document.getElementById('connectionStatus'),
|
|
sdkVersion: document.getElementById('sdkVersion')
|
|
};
|
|
|
|
const fullscreenState = {
|
|
isFullscreen: false,
|
|
sdkInitialized: false
|
|
};
|
|
|
|
const panelState = {
|
|
configPanel: false,
|
|
captionPanel: false,
|
|
logPanel: false
|
|
};
|
|
|
|
const logger = {
|
|
log(level, message, data = null) {
|
|
const timestamp = new Date().toLocaleTimeString('zh-CN', { hour12: false });
|
|
const entry = document.createElement('div');
|
|
entry.className = 'log-entry';
|
|
|
|
const levelClass = level.toLowerCase();
|
|
const displayMessage = data ? `${message} ${JSON.stringify(data, null, 2)}` : message;
|
|
|
|
entry.innerHTML = `
|
|
<span class="log-time">[${timestamp}]</span>
|
|
<span class="log-level ${levelClass}">${level}</span>
|
|
<span class="log-message">${displayMessage}</span>
|
|
`;
|
|
|
|
elements.logContainer.appendChild(entry);
|
|
elements.logContainer.scrollTop = elements.logContainer.scrollHeight;
|
|
|
|
console[level === 'ERROR' ? 'error' : level === 'WARN' ? 'warn' : 'log'](message, data);
|
|
},
|
|
|
|
debug(message, data) { this.log('DEBUG', message, data); },
|
|
info(message, data) { this.log('INFO', message, data); },
|
|
warn(message, data) { this.log('WARN', message, data); },
|
|
error(message, data) { this.log('ERROR', message, data); }
|
|
};
|
|
|
|
const eventListeners = {
|
|
[ICSEventKey.ERROR]: (error) => {
|
|
logger.error('SDK 错误', error);
|
|
updateConnectionStatus(false);
|
|
},
|
|
|
|
[ICSEventKey.JOB_INFO_CHANGE]: (jobInfo) => {
|
|
logger.info('任务信息变更', jobInfo);
|
|
state.jobInfo = jobInfo;
|
|
},
|
|
|
|
[ICSEventKey.SPEAKING_START]: () => {
|
|
logger.info('数字人开始说话');
|
|
state.isSpeaking = true;
|
|
updateUIState();
|
|
},
|
|
|
|
[ICSEventKey.SPEAKING_STOP]: () => {
|
|
logger.info('数字人停止说话');
|
|
state.isSpeaking = false;
|
|
updateUIState();
|
|
},
|
|
|
|
[ICSEventKey.SPEECH_RECOGNIZED]: (data) => {
|
|
logger.info('语音识别结果', data);
|
|
},
|
|
|
|
[ICSEventKey.SEMANTIC_RECOGNIZED]: (data) => {
|
|
logger.info('语义识别结果', data);
|
|
},
|
|
|
|
[ICSEventKey.ENTER_SLEEP]: () => {
|
|
logger.info('数字人进入休眠状态');
|
|
},
|
|
|
|
[ICSEventKey.ENTER_ACTIVE]: () => {
|
|
logger.info('数字人进入激活状态');
|
|
},
|
|
|
|
[ICSEventKey.LANGUAGE_INFO_CHANGE]: (langInfo) => {
|
|
logger.info('语言信息变更', langInfo);
|
|
},
|
|
|
|
[ICSEventKey.JOB_END_MENTION]: (data) => {
|
|
logger.warn('任务即将结束', data);
|
|
},
|
|
|
|
[ICSEventKey.JOB_END]: (data) => {
|
|
logger.info('任务已结束', data);
|
|
state.isChatting = false;
|
|
state.jobInfo = null;
|
|
updateConnectionStatus(false);
|
|
updateUIState();
|
|
}
|
|
};
|
|
|
|
function updateUIState() {
|
|
const { isInitialized } = state;
|
|
|
|
elements.initBtn.disabled = isInitialized;
|
|
elements.destroyBtn.disabled = !isInitialized;
|
|
}
|
|
|
|
function updateConnectionStatus(connected) {
|
|
const status = elements.connectionStatus;
|
|
if (connected) {
|
|
status.textContent = '已连接';
|
|
status.className = 'status connected';
|
|
} else {
|
|
status.textContent = '未连接';
|
|
status.className = 'status disconnected';
|
|
}
|
|
}
|
|
|
|
function togglePanel(panelId) {
|
|
const panel = document.getElementById(panelId);
|
|
console.log('切换面板:', panelId, '找到元素:', !!panel);
|
|
if (!panel) return;
|
|
|
|
const isVisible = panel.classList.contains('visible');
|
|
console.log('当前可见:', isVisible);
|
|
|
|
Object.keys(panelState).forEach(key => {
|
|
const el = document.getElementById(key);
|
|
if (el) el.classList.remove('visible');
|
|
panelState[key] = false;
|
|
});
|
|
|
|
document.querySelectorAll('.toolbar-btn').forEach(btn => btn.classList.remove('active'));
|
|
|
|
if (!isVisible) {
|
|
panel.classList.add('visible');
|
|
panelState[panelId] = true;
|
|
console.log('面板已显示:', panelId);
|
|
|
|
const btnMap = {
|
|
configPanel: 'toggleConfigBtn',
|
|
captionPanel: 'toggleCaptionBtn',
|
|
logPanel: 'toggleLogBtn'
|
|
};
|
|
|
|
const btnId = btnMap[panelId];
|
|
if (btnId && elements[btnId]) {
|
|
elements[btnId].classList.add('active');
|
|
}
|
|
}
|
|
}
|
|
|
|
async function initializeSDK() {
|
|
try {
|
|
const serverAddress = elements.serverAddress.value.trim();
|
|
const robotId = elements.robotId.value.trim();
|
|
let onceCode = elements.onceCode.value.trim();
|
|
|
|
if (!serverAddress) {
|
|
logger.error('请输入服务器地址');
|
|
return;
|
|
}
|
|
|
|
if (!onceCode) {
|
|
logger.info('授权码为空,正在自动获取...');
|
|
try {
|
|
onceCode = await fetchOnceCode();
|
|
elements.onceCode.value = onceCode;
|
|
logger.info('授权码自动获取成功');
|
|
} catch (err) {
|
|
logger.error('授权码自动获取失败', err.message);
|
|
return;
|
|
}
|
|
}
|
|
|
|
logger.info('正在初始化 SDK...');
|
|
|
|
const isSupported = await HwICSUiSdk.checkBrowserSupport(true);
|
|
if (!isSupported) {
|
|
logger.error('当前浏览器不支持该功能');
|
|
return;
|
|
}
|
|
|
|
const logLevel = elements.logLevel.value;
|
|
HwICSUiSdk.setLogLevel(logLevel);
|
|
HwICSUiSdk.addEventListeners(eventListeners);
|
|
|
|
const createParam = {
|
|
serverAddress,
|
|
containerId: 'digitalHumanBox',
|
|
robotId: robotId || undefined,
|
|
onceCode: onceCode || undefined,
|
|
config: {
|
|
[ConfigKey.ENABLE_CAPTION]: true,
|
|
[ConfigKey.ENABLE_CHAT_BTN]: true,
|
|
[ConfigKey.ENABLE_HOT_ISSUES]: true,
|
|
[ConfigKey.ENABLE_WEAK_ERROR_INFO]: false,
|
|
[ConfigKey.SHOW_LANGUAGE_UI]: false,
|
|
[ConfigKey.ENABLE_DEVICE_SETTING]: false,
|
|
[ConfigKey.ENABLE_NETWORK_CHECK]: false,
|
|
[ConfigKey.AUTO_PLAY_REMOTE_VIDEO]: true,
|
|
[ConfigKey.DEFAULT_MUTE_REMOTE_AUDIO]: false
|
|
},
|
|
logLevel
|
|
};
|
|
|
|
await HwICSUiSdk.create(createParam);
|
|
|
|
state.isInitialized = true;
|
|
fullscreenState.sdkInitialized = true;
|
|
updateConnectionStatus(true);
|
|
updateUIState();
|
|
|
|
elements.sdkVersion.textContent = `v${HwICSUiSdk.version}`;
|
|
|
|
logger.info('SDK 初始化成功', { version: HwICSUiSdk.version });
|
|
|
|
rebindDynamicButtons();
|
|
setTimeout(calculateScale, 500);
|
|
|
|
} catch (error) {
|
|
logger.error('SDK 初始化失败', error);
|
|
state.isInitialized = false;
|
|
updateUIState();
|
|
}
|
|
}
|
|
|
|
async function destroySDK() {
|
|
try {
|
|
logger.info('正在销毁 SDK...');
|
|
|
|
await HwICSUiSdk.destroy();
|
|
|
|
state.isInitialized = false;
|
|
state.isChatting = false;
|
|
state.isSpeaking = false;
|
|
state.jobInfo = null;
|
|
fullscreenState.sdkInitialized = false;
|
|
|
|
updateConnectionStatus(false);
|
|
updateUIState();
|
|
|
|
elements.digitalHumanBox.innerHTML = `
|
|
<div class="fullscreen-exit-hint">按 ESC 退出全屏</div>
|
|
<div class="placeholder">
|
|
<span class="placeholder-icon">🤖</span>
|
|
<p>点击右下角 ⚙️ 按钮初始化 SDK</p>
|
|
</div>
|
|
`;
|
|
|
|
logger.info('SDK 已销毁');
|
|
|
|
} catch (error) {
|
|
logger.error('销毁 SDK 失败', error);
|
|
}
|
|
}
|
|
|
|
async function handleGetOnceCode() {
|
|
try {
|
|
elements.getOnceCodeBtn.disabled = true;
|
|
elements.getOnceCodeBtn.textContent = '🔄 获取中...';
|
|
logger.info('正在获取授权码...');
|
|
|
|
const onceCode = await fetchOnceCode();
|
|
elements.onceCode.value = onceCode;
|
|
logger.info('授权码获取成功');
|
|
|
|
} catch (error) {
|
|
logger.error('获取授权码失败', error.message);
|
|
} finally {
|
|
elements.getOnceCodeBtn.disabled = false;
|
|
elements.getOnceCodeBtn.textContent = '🔑 获取';
|
|
}
|
|
}
|
|
|
|
function clearLogs() {
|
|
elements.logContainer.innerHTML = '';
|
|
logger.info('日志已清空');
|
|
}
|
|
|
|
function applyCaptionStyle() {
|
|
const width = elements.captionWidth.value;
|
|
const height = elements.captionHeight.value;
|
|
const fontSize = elements.captionFontSize.value;
|
|
const right = elements.captionRight.value;
|
|
const bottom = elements.captionBottom.value;
|
|
|
|
const captionContainer = document.querySelector('.caption-container-hub');
|
|
if (!captionContainer) {
|
|
logger.warn('字幕容器未找到');
|
|
return;
|
|
}
|
|
|
|
captionContainer.style.position = 'fixed';
|
|
captionContainer.style.width = `${width}px`;
|
|
captionContainer.style.height = `${height}px`;
|
|
captionContainer.style.maxHeight = `${height}px`;
|
|
captionContainer.style.right = `${right}px`;
|
|
captionContainer.style.bottom = `${bottom}px`;
|
|
captionContainer.style.fontSize = `${fontSize}px`;
|
|
captionContainer.style.borderRadius = '8px';
|
|
captionContainer.style.padding = '10px';
|
|
captionContainer.style.overflowY = 'auto';
|
|
captionContainer.style.cursor = 'move';
|
|
captionContainer.style.userSelect = 'none';
|
|
captionContainer.style.zIndex = '9999';
|
|
|
|
const textElements = captionContainer.querySelectorAll('*');
|
|
textElements.forEach(el => {
|
|
el.style.fontSize = `${fontSize}px`;
|
|
});
|
|
|
|
initCaptionDragAndResize(captionContainer);
|
|
|
|
logger.info('字幕容器样式已应用', { width, height, fontSize, right, bottom });
|
|
}
|
|
|
|
function initCaptionDragAndResize(container) {
|
|
container.dataset.dragInitialized = 'true';
|
|
|
|
let isDragging = false;
|
|
let isResizing = false;
|
|
let startX, startY, startRight, startBottom, startWidth, startHeight;
|
|
|
|
let resizeHandle = container.querySelector('.caption-resize-handle');
|
|
if (!resizeHandle) {
|
|
resizeHandle = document.createElement('div');
|
|
resizeHandle.className = 'caption-resize-handle';
|
|
resizeHandle.style.position = 'absolute';
|
|
resizeHandle.style.right = '0';
|
|
resizeHandle.style.bottom = '0';
|
|
resizeHandle.style.width = '20px';
|
|
resizeHandle.style.height = '20px';
|
|
resizeHandle.style.cursor = 'nwse-resize';
|
|
resizeHandle.style.background = 'linear-gradient(135deg, transparent 50%, rgba(255,255,255,0.5) 50%)';
|
|
resizeHandle.style.borderRadius = '0 0 8px 0';
|
|
resizeHandle.style.zIndex = '10000';
|
|
container.appendChild(resizeHandle);
|
|
}
|
|
|
|
container.onmousedown = (e) => {
|
|
if (e.target === resizeHandle || resizeHandle.contains(e.target)) return;
|
|
isDragging = true;
|
|
startX = e.clientX;
|
|
startY = e.clientY;
|
|
const rect = container.getBoundingClientRect();
|
|
startRight = window.innerWidth - rect.right;
|
|
startBottom = window.innerHeight - rect.bottom;
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
};
|
|
|
|
resizeHandle.onmousedown = (e) => {
|
|
isResizing = true;
|
|
startX = e.clientX;
|
|
startY = e.clientY;
|
|
startWidth = container.offsetWidth;
|
|
startHeight = container.offsetHeight;
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
};
|
|
|
|
document.onmousemove = (e) => {
|
|
if (!isDragging && !isResizing) return;
|
|
e.preventDefault();
|
|
|
|
if (isDragging) {
|
|
const dx = e.clientX - startX;
|
|
const dy = e.clientY - startY;
|
|
container.style.right = `${startRight - dx}px`;
|
|
container.style.bottom = `${startBottom + dy}px`;
|
|
}
|
|
if (isResizing) {
|
|
const dx = e.clientX - startX;
|
|
const dy = e.clientY - startY;
|
|
const newWidth = Math.max(200, startWidth + dx);
|
|
const newHeight = Math.max(100, startHeight + dy);
|
|
container.style.width = `${newWidth}px`;
|
|
container.style.height = `${newHeight}px`;
|
|
container.style.maxHeight = `${newHeight}px`;
|
|
}
|
|
};
|
|
|
|
document.onmouseup = () => {
|
|
isDragging = false;
|
|
isResizing = false;
|
|
};
|
|
|
|
logger.info('字幕容器拖拽和缩放已启用');
|
|
}
|
|
|
|
function watchCaptionContainer() {
|
|
const observer = new MutationObserver((mutations) => {
|
|
for (const mutation of mutations) {
|
|
if (mutation.type === 'childList') {
|
|
const captionContainer = document.querySelector('.caption-container-hub');
|
|
if (captionContainer && !captionContainer.dataset.dragInitialized) {
|
|
logger.info('检测到字幕容器,自动启用拖拽功能');
|
|
captionContainer.style.position = 'fixed';
|
|
captionContainer.style.cursor = 'move';
|
|
captionContainer.style.userSelect = 'none';
|
|
captionContainer.style.zIndex = '9999';
|
|
initCaptionDragAndResize(captionContainer);
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
observer.observe(document.body, {
|
|
childList: true,
|
|
subtree: true
|
|
});
|
|
|
|
logger.info('已启动字幕容器监听');
|
|
}
|
|
|
|
function calculateScale() {
|
|
const container = elements.digitalHumanBox;
|
|
const sdkContainer = container.querySelector('.digital-intelligent-interaction');
|
|
|
|
if (!sdkContainer) return;
|
|
|
|
sdkContainer.style.transform = 'none';
|
|
}
|
|
|
|
function toggleFullscreen() {
|
|
if (!fullscreenState.sdkInitialized) {
|
|
logger.warn('请先初始化 SDK');
|
|
return;
|
|
}
|
|
|
|
fullscreenState.isFullscreen = !fullscreenState.isFullscreen;
|
|
|
|
if (fullscreenState.isFullscreen) {
|
|
elements.digitalHumanBox.classList.add('fullscreen');
|
|
elements.fullscreenBtn.innerHTML = '⛶';
|
|
elements.fullscreenBtn.title = '退出全屏';
|
|
logger.info('进入全屏模式');
|
|
} else {
|
|
elements.digitalHumanBox.classList.remove('fullscreen');
|
|
elements.fullscreenBtn.innerHTML = '⛶';
|
|
elements.fullscreenBtn.title = '全屏显示';
|
|
logger.info('退出全屏模式');
|
|
}
|
|
|
|
setTimeout(calculateScale, 100);
|
|
}
|
|
|
|
function handleFullscreenKeydown(e) {
|
|
if (e.key === 'Escape' && fullscreenState.isFullscreen) {
|
|
toggleFullscreen();
|
|
}
|
|
}
|
|
|
|
function initScaling() {
|
|
const resizeObserver = new ResizeObserver(() => {
|
|
if (fullscreenState.sdkInitialized) {
|
|
calculateScale();
|
|
}
|
|
});
|
|
|
|
resizeObserver.observe(elements.digitalHumanBox);
|
|
|
|
window.addEventListener('keydown', handleFullscreenKeydown);
|
|
}
|
|
|
|
function bindEvents() {
|
|
elements.initBtn.addEventListener('click', initializeSDK);
|
|
elements.destroyBtn.addEventListener('click', destroySDK);
|
|
elements.getOnceCodeBtn.addEventListener('click', handleGetOnceCode);
|
|
elements.applyCaptionStyleBtn.addEventListener('click', applyCaptionStyle);
|
|
|
|
elements.clearLogBtn.addEventListener('click', clearLogs);
|
|
elements.logLevel.addEventListener('change', () => {
|
|
if (state.isInitialized) {
|
|
HwICSUiSdk.setLogLevel(elements.logLevel.value);
|
|
logger.info(`日志级别已切换为: ${elements.logLevel.value}`);
|
|
}
|
|
});
|
|
|
|
elements.toggleConfigBtn.addEventListener('click', () => togglePanel('configPanel'));
|
|
elements.toggleCaptionBtn.addEventListener('click', () => togglePanel('captionPanel'));
|
|
elements.toggleLogBtn.addEventListener('click', () => togglePanel('logPanel'));
|
|
elements.fullscreenBtn.addEventListener('click', toggleFullscreen);
|
|
|
|
document.querySelectorAll('.panel-close').forEach(btn => {
|
|
btn.addEventListener('click', () => {
|
|
const panelId = btn.dataset.panel;
|
|
if (panelId) togglePanel(panelId);
|
|
});
|
|
});
|
|
|
|
window.addEventListener('beforeunload', () => {
|
|
if (state.isInitialized) {
|
|
HwICSUiSdk.destroy();
|
|
}
|
|
});
|
|
}
|
|
|
|
function rebindDynamicButtons() {
|
|
const fullscreenBtn = document.getElementById('fullscreenBtn');
|
|
if (fullscreenBtn) {
|
|
fullscreenBtn.removeEventListener('click', toggleFullscreen);
|
|
fullscreenBtn.addEventListener('click', toggleFullscreen);
|
|
}
|
|
}
|
|
|
|
function init() {
|
|
logger.info('应用初始化完成');
|
|
logger.info('HwICSUiSdk 版本', { version: HwICSUiSdk.version, buildTime: HwICSUiSdk.buildTime });
|
|
|
|
bindEvents();
|
|
updateUIState();
|
|
initScaling();
|
|
watchCaptionContainer();
|
|
}
|
|
|
|
init();
|