feat(多语言): 添加提示文本的多语言支持
- 在状态管理中新增当前语言和可用语言列表字段 - 为提示文本添加多语言输入框生成功能 - 实现根据当前语言自动更新提示文本显示 - 重构重启对话逻辑为独立函数并添加重试机制 - 将提示文本多语言配置保存到本地存储
This commit is contained in:
@@ -146,6 +146,9 @@
|
|||||||
<label for="tipPadding">内边距 (px)</label>
|
<label for="tipPadding">内边距 (px)</label>
|
||||||
<input type="number" id="tipPadding" value="6" min="0" max="20">
|
<input type="number" id="tipPadding" value="6" min="0" max="20">
|
||||||
</div>
|
</div>
|
||||||
|
<div id="tipTextFieldsContainer">
|
||||||
|
<!-- 由 JS 根据 SDK 返回的多语言列表动态生成 -->
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 消息气泡样式 -->
|
<!-- 消息气泡样式 -->
|
||||||
|
|||||||
@@ -52,7 +52,10 @@ const state = {
|
|||||||
isMuted: false,
|
isMuted: false,
|
||||||
jobInfo: null,
|
jobInfo: null,
|
||||||
currentInteractionMode: InteractionMode.AUDIO,
|
currentInteractionMode: InteractionMode.AUDIO,
|
||||||
keepAliveActive: false
|
keepAliveActive: false,
|
||||||
|
currentLanguage: 'zh_CN',
|
||||||
|
availableLanguages: [],
|
||||||
|
tipTextValues: {}
|
||||||
};
|
};
|
||||||
|
|
||||||
const elements = {
|
const elements = {
|
||||||
@@ -86,6 +89,7 @@ const elements = {
|
|||||||
tipBgOpacity: document.getElementById('tipBgOpacity'),
|
tipBgOpacity: document.getElementById('tipBgOpacity'),
|
||||||
tipBorderRadius: document.getElementById('tipBorderRadius'),
|
tipBorderRadius: document.getElementById('tipBorderRadius'),
|
||||||
tipPadding: document.getElementById('tipPadding'),
|
tipPadding: document.getElementById('tipPadding'),
|
||||||
|
tipTextFieldsContainer: document.getElementById('tipTextFieldsContainer'),
|
||||||
|
|
||||||
messageFontSize: document.getElementById('messageFontSize'),
|
messageFontSize: document.getElementById('messageFontSize'),
|
||||||
messageFontWeight: document.getElementById('messageFontWeight'),
|
messageFontWeight: document.getElementById('messageFontWeight'),
|
||||||
@@ -220,6 +224,15 @@ const eventListeners = {
|
|||||||
[ICSEventKey.ERROR]: (error) => {
|
[ICSEventKey.ERROR]: (error) => {
|
||||||
logger.error('SDK 错误', error);
|
logger.error('SDK 错误', error);
|
||||||
updateConnectionStatus(false);
|
updateConnectionStatus(false);
|
||||||
|
if (error?.error_code === '999400003' || error?.errorMsg?.includes('MUTE_TIMEOUT')) {
|
||||||
|
logger.warn('检测到静默超时,正在重新开始对话...');
|
||||||
|
state.isChatting = false;
|
||||||
|
state.jobInfo = null;
|
||||||
|
updateUIState();
|
||||||
|
if (state.keepAliveActive) {
|
||||||
|
setTimeout(() => restartChat(), 2000);
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
[ICSEventKey.JOB_INFO_CHANGE]: (jobInfo) => {
|
[ICSEventKey.JOB_INFO_CHANGE]: (jobInfo) => {
|
||||||
@@ -270,38 +283,7 @@ const eventListeners = {
|
|||||||
state.jobInfo = null;
|
state.jobInfo = null;
|
||||||
updateUIState();
|
updateUIState();
|
||||||
if (state.keepAliveActive) {
|
if (state.keepAliveActive) {
|
||||||
setTimeout(async () => {
|
setTimeout(() => restartChat(), 2000);
|
||||||
try {
|
|
||||||
logger.info('正在调用 startChat 重新开始对话...');
|
|
||||||
const chatResult = await HwICSUiSdk.startChat({
|
|
||||||
interactionMode: InteractionMode.AUDIO,
|
|
||||||
wakeupType: WakeupType.VOICE,
|
|
||||||
preCheckPermissions: false
|
|
||||||
});
|
|
||||||
if (chatResult.result) {
|
|
||||||
logger.info('重新开始对话成功', chatResult);
|
|
||||||
state.isChatting = true;
|
|
||||||
updateUIState();
|
|
||||||
} else {
|
|
||||||
logger.warn('重新开始对话返回失败', chatResult);
|
|
||||||
if (chatResult.errorCode === '999400004') {
|
|
||||||
logger.info('对话已存在,尝试 activeInteractionMode');
|
|
||||||
await HwICSUiSdk.activeInteractionMode();
|
|
||||||
state.isChatting = true;
|
|
||||||
updateUIState();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
logger.error('重新激活失败', error);
|
|
||||||
try {
|
|
||||||
await HwICSUiSdk.activeInteractionMode();
|
|
||||||
state.isChatting = true;
|
|
||||||
updateUIState();
|
|
||||||
} catch (e) {
|
|
||||||
logger.error('activeInteractionMode 也失败', e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, 2000);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -309,6 +291,48 @@ const eventListeners = {
|
|||||||
let keepAliveTimer = null;
|
let keepAliveTimer = null;
|
||||||
const KEEP_ALIVE_INTERVAL = 10000;
|
const KEEP_ALIVE_INTERVAL = 10000;
|
||||||
|
|
||||||
|
async function restartChat(retryCount = 0) {
|
||||||
|
const MAX_RETRIES = 3;
|
||||||
|
try {
|
||||||
|
logger.info('正在调用 startChat 重新开始对话...');
|
||||||
|
const chatResult = await HwICSUiSdk.startChat({
|
||||||
|
interactionMode: InteractionMode.AUDIO,
|
||||||
|
wakeupType: WakeupType.VOICE,
|
||||||
|
preCheckPermissions: false
|
||||||
|
});
|
||||||
|
if (chatResult.result) {
|
||||||
|
logger.info('重新开始对话成功', chatResult);
|
||||||
|
state.isChatting = true;
|
||||||
|
updateUIState();
|
||||||
|
} else {
|
||||||
|
logger.warn('重新开始对话返回失败', chatResult);
|
||||||
|
if (chatResult.errorCode === '999400004') {
|
||||||
|
logger.info('对话已存在,尝试 activeInteractionMode');
|
||||||
|
await HwICSUiSdk.activeInteractionMode();
|
||||||
|
state.isChatting = true;
|
||||||
|
updateUIState();
|
||||||
|
} else if (retryCount < MAX_RETRIES) {
|
||||||
|
logger.info(`将在 3 秒后重试 (${retryCount + 1}/${MAX_RETRIES})`);
|
||||||
|
setTimeout(() => restartChat(retryCount + 1), 3000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('重新激活失败', error);
|
||||||
|
if (retryCount < MAX_RETRIES) {
|
||||||
|
logger.info(`将在 3 秒后重试 (${retryCount + 1}/${MAX_RETRIES})`);
|
||||||
|
setTimeout(() => restartChat(retryCount + 1), 3000);
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
await HwICSUiSdk.activeInteractionMode();
|
||||||
|
state.isChatting = true;
|
||||||
|
updateUIState();
|
||||||
|
} catch (e) {
|
||||||
|
logger.error('activeInteractionMode 也失败', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function startKeepAlive() {
|
function startKeepAlive() {
|
||||||
if (keepAliveTimer) return;
|
if (keepAliveTimer) return;
|
||||||
state.keepAliveActive = true;
|
state.keepAliveActive = true;
|
||||||
@@ -409,6 +433,8 @@ async function loadLanguageOptions() {
|
|||||||
if (langInfo.currentLanguage) {
|
if (langInfo.currentLanguage) {
|
||||||
elements.languageSelect.value = langInfo.currentLanguage;
|
elements.languageSelect.value = langInfo.currentLanguage;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
generateTipTextFields(langInfo.languageList);
|
||||||
} else {
|
} else {
|
||||||
elements.languageSelect.innerHTML = '<option value="">无可用语言</option>';
|
elements.languageSelect.innerHTML = '<option value="">无可用语言</option>';
|
||||||
}
|
}
|
||||||
@@ -429,10 +455,14 @@ function updateLanguageSelect(langInfo) {
|
|||||||
option.textContent = item.language_desc;
|
option.textContent = item.language_desc;
|
||||||
elements.languageSelect.appendChild(option);
|
elements.languageSelect.appendChild(option);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
generateTipTextFields(langInfo.languageList);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (langInfo.currentLanguage) {
|
if (langInfo.currentLanguage) {
|
||||||
elements.languageSelect.value = langInfo.currentLanguage;
|
elements.languageSelect.value = langInfo.currentLanguage;
|
||||||
|
state.currentLanguage = langInfo.currentLanguage;
|
||||||
|
updateTipTextFromState();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -611,6 +641,94 @@ function clearLogs() {
|
|||||||
logger.info('日志已清空');
|
logger.info('日志已清空');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function loadTipTextFields() {
|
||||||
|
if (state.availableLanguages.length > 0) {
|
||||||
|
generateTipTextFields(state.availableLanguages);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!state.isInitialized) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const langInfo = await HwICSUiSdk.getLanguageInfo();
|
||||||
|
if (langInfo.languageList && langInfo.languageList.length > 0) {
|
||||||
|
state.availableLanguages = langInfo.languageList;
|
||||||
|
generateTipTextFields(langInfo.languageList);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.warn('获取语言信息用于提示文本失败', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateTipTextFields(languageList) {
|
||||||
|
if (!elements.tipTextFieldsContainer) return;
|
||||||
|
|
||||||
|
elements.tipTextFieldsContainer.innerHTML = '';
|
||||||
|
|
||||||
|
if (!languageList || languageList.length === 0) {
|
||||||
|
elements.tipTextFieldsContainer.innerHTML = '<div class="form-group"><label>提示文本</label><p style="color: var(--text-muted); font-size: 0.85rem;">暂无可用的语言信息</p></div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
state.availableLanguages = languageList;
|
||||||
|
|
||||||
|
languageList.forEach(item => {
|
||||||
|
const langCode = item.language;
|
||||||
|
const langDesc = item.language_desc;
|
||||||
|
|
||||||
|
if (!state.tipTextValues[langCode]) {
|
||||||
|
const defaultTexts = {
|
||||||
|
'zh_CN': '点击开始对话',
|
||||||
|
'en_US': 'Click to start conversation'
|
||||||
|
};
|
||||||
|
state.tipTextValues[langCode] = defaultTexts[langCode] || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
const group = document.createElement('div');
|
||||||
|
group.className = 'form-group';
|
||||||
|
group.dataset.langCode = langCode;
|
||||||
|
|
||||||
|
const label = document.createElement('label');
|
||||||
|
label.setAttribute('for', `tipText_${langCode}`);
|
||||||
|
label.textContent = `提示文本 (${langDesc})`;
|
||||||
|
|
||||||
|
const input = document.createElement('input');
|
||||||
|
input.type = 'text';
|
||||||
|
input.id = `tipText_${langCode}`;
|
||||||
|
input.value = state.tipTextValues[langCode];
|
||||||
|
input.placeholder = `请输入${langDesc}提示文本`;
|
||||||
|
|
||||||
|
input.addEventListener('input', () => {
|
||||||
|
state.tipTextValues[langCode] = input.value;
|
||||||
|
const tipEl = document.querySelector('.caption-container-hub .layout-hub-tip');
|
||||||
|
if (tipEl && state.currentLanguage === langCode) {
|
||||||
|
tipEl.textContent = input.value;
|
||||||
|
}
|
||||||
|
saveCaptionStyleToStorage();
|
||||||
|
});
|
||||||
|
|
||||||
|
group.appendChild(label);
|
||||||
|
group.appendChild(input);
|
||||||
|
elements.tipTextFieldsContainer.appendChild(group);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateTipText(tipEl) {
|
||||||
|
const currentLang = state.currentLanguage || 'zh_CN';
|
||||||
|
const text = state.tipTextValues[currentLang] || state.tipTextValues['zh_CN'] || '';
|
||||||
|
if (tipEl) {
|
||||||
|
tipEl.textContent = text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateTipTextFromState() {
|
||||||
|
const tipEl = document.querySelector('.caption-container-hub .layout-hub-tip');
|
||||||
|
if (!tipEl) return;
|
||||||
|
const currentLang = state.currentLanguage || 'zh_CN';
|
||||||
|
const text = state.tipTextValues[currentLang] || state.tipTextValues['zh_CN'] || '';
|
||||||
|
tipEl.textContent = text;
|
||||||
|
}
|
||||||
|
|
||||||
function applyCaptionStyle() {
|
function applyCaptionStyle() {
|
||||||
const captionContainer = document.querySelector('.caption-container-hub');
|
const captionContainer = document.querySelector('.caption-container-hub');
|
||||||
if (!captionContainer) {
|
if (!captionContainer) {
|
||||||
@@ -636,6 +754,7 @@ function applyCaptionStyle() {
|
|||||||
tipEl.style.backgroundColor = hexToRgba(elements.tipBgColor.value, elements.tipBgOpacity.value / 100);
|
tipEl.style.backgroundColor = hexToRgba(elements.tipBgColor.value, elements.tipBgOpacity.value / 100);
|
||||||
tipEl.style.borderRadius = `${elements.tipBorderRadius.value}px`;
|
tipEl.style.borderRadius = `${elements.tipBorderRadius.value}px`;
|
||||||
tipEl.style.padding = `${elements.tipPadding.value}px`;
|
tipEl.style.padding = `${elements.tipPadding.value}px`;
|
||||||
|
updateTipText(tipEl);
|
||||||
}
|
}
|
||||||
|
|
||||||
const messageItems = captionContainer.querySelectorAll('.message-item .item-content');
|
const messageItems = captionContainer.querySelectorAll('.message-item .item-content');
|
||||||
@@ -896,6 +1015,7 @@ function saveCaptionStyleToStorage() {
|
|||||||
tipBgOpacity: elements.tipBgOpacity.value,
|
tipBgOpacity: elements.tipBgOpacity.value,
|
||||||
tipBorderRadius: elements.tipBorderRadius.value,
|
tipBorderRadius: elements.tipBorderRadius.value,
|
||||||
tipPadding: elements.tipPadding.value,
|
tipPadding: elements.tipPadding.value,
|
||||||
|
tipTextValues: state.tipTextValues,
|
||||||
messageFontSize: elements.messageFontSize.value,
|
messageFontSize: elements.messageFontSize.value,
|
||||||
messageFontWeight: elements.messageFontWeight.value,
|
messageFontWeight: elements.messageFontWeight.value,
|
||||||
messageColor: elements.messageColor.value,
|
messageColor: elements.messageColor.value,
|
||||||
@@ -978,6 +1098,9 @@ function applySavedCaptionStyle(savedData) {
|
|||||||
if (savedData.hotIssuesBgOpacity) elements.hotIssuesBgOpacity.value = savedData.hotIssuesBgOpacity;
|
if (savedData.hotIssuesBgOpacity) elements.hotIssuesBgOpacity.value = savedData.hotIssuesBgOpacity;
|
||||||
if (savedData.hotIssuesBorderRadius) elements.hotIssuesBorderRadius.value = savedData.hotIssuesBorderRadius;
|
if (savedData.hotIssuesBorderRadius) elements.hotIssuesBorderRadius.value = savedData.hotIssuesBorderRadius;
|
||||||
if (savedData.hotIssuesPadding) elements.hotIssuesPadding.value = savedData.hotIssuesPadding;
|
if (savedData.hotIssuesPadding) elements.hotIssuesPadding.value = savedData.hotIssuesPadding;
|
||||||
|
if (savedData.tipTextValues) {
|
||||||
|
state.tipTextValues = savedData.tipTextValues;
|
||||||
|
}
|
||||||
|
|
||||||
document.querySelectorAll('#captionPanel input[type="range"]').forEach(slider => {
|
document.querySelectorAll('#captionPanel input[type="range"]').forEach(slider => {
|
||||||
const valueSpan = slider.nextElementSibling;
|
const valueSpan = slider.nextElementSibling;
|
||||||
@@ -1397,6 +1520,7 @@ function bindEvents() {
|
|||||||
elements.toggleConfigBtn.addEventListener('click', () => togglePanel('configPanel'));
|
elements.toggleConfigBtn.addEventListener('click', () => togglePanel('configPanel'));
|
||||||
elements.toggleCaptionBtn.addEventListener('click', () => {
|
elements.toggleCaptionBtn.addEventListener('click', () => {
|
||||||
togglePanel('captionPanel');
|
togglePanel('captionPanel');
|
||||||
|
loadTipTextFields();
|
||||||
if (!captionStylePromptShown && hasSavedCaptionStyle()) {
|
if (!captionStylePromptShown && hasSavedCaptionStyle()) {
|
||||||
captionStylePromptShown = true;
|
captionStylePromptShown = true;
|
||||||
showCaptionStylePrompt((applySaved) => {
|
showCaptionStylePrompt((applySaved) => {
|
||||||
@@ -1428,6 +1552,8 @@ function bindEvents() {
|
|||||||
loadLanguageOptions();
|
loadLanguageOptions();
|
||||||
});
|
});
|
||||||
elements.languageSelect.addEventListener('change', (e) => {
|
elements.languageSelect.addEventListener('change', (e) => {
|
||||||
|
state.currentLanguage = e.target.value;
|
||||||
|
updateTipTextFromState();
|
||||||
changeLanguage(e.target.value);
|
changeLanguage(e.target.value);
|
||||||
});
|
});
|
||||||
elements.fullscreenBtn.addEventListener('click', toggleFullscreen);
|
elements.fullscreenBtn.addEventListener('click', toggleFullscreen);
|
||||||
|
|||||||
Reference in New Issue
Block a user