Add voice name flow controls and analytics

This commit is contained in:
Yera All
2026-04-05 03:26:18 +05:00
parent d15fcf7129
commit d959c2b2f0
31 changed files with 6410 additions and 152 deletions
+217 -6
View File
@@ -10,6 +10,7 @@ const state = {
loginPath: '/auth/oidc/start?return_mode=popup',
providerLabel: 'Keycloak',
},
voiceNameConfig: null,
};
const $ = (id) => document.getElementById(id);
@@ -290,8 +291,8 @@ function defaultIvrFlowDocument() {
invalid_target_node_id: 'root',
no_input_target_node_id: 'root',
options: [
{ digit: '1', target_node_id: 'menu_kz' },
{ digit: '2', target_node_id: 'menu_ru' },
{ digit: '1', target_node_id: 'voice_start_kz' },
{ digit: '2', target_node_id: 'voice_start_ru' },
],
},
{
@@ -362,9 +363,60 @@ function defaultIvrFlowDocument() {
};
}
function defaultVoiceStartIvrFlowDocument() {
return {
nodes: [
{
node_id: 'root',
prompt_text:
'\u0421\u0430\u043b\u0430\u043c\u0430\u0442\u0441\u044b\u0437 \u0431\u0430! \u049a\u0430\u0437\u0430\u049b \u0442\u0456\u043b\u0456\u043d \u0442\u0430\u04a3\u0434\u0430\u0443 \u04af\u0448\u0456\u043d \u0431\u0456\u0440 \u0446\u0438\u0444\u0440\u044b\u043d \u0442\u0435\u0440\u0456\u04a3\u0456\u0437. \u0417\u0434\u0440\u0430\u0432\u0441\u0442\u0432\u0443\u0439\u0442\u0435! \u0414\u043e\u0431\u0440\u043e \u043f\u043e\u0436\u0430\u043b\u043e\u0432\u0430\u0442\u044c \u0432 \u043a\u043e\u043d\u0442\u0430\u043a\u0442-\u0446\u0435\u043d\u0442\u0440. \u0414\u043b\u044f \u0440\u0443\u0441\u0441\u043a\u043e\u0433\u043e \u044f\u0437\u044b\u043a\u0430 \u043d\u0430\u0436\u043c\u0438\u0442\u0435 \u0446\u0438\u0444\u0440\u0443 \u0434\u0432\u0430.',
prompt_sequence: [
{
prompt_audio_key: 'ivr/demo-language-kz',
prompt_text:
'\u0421\u0430\u043b\u0430\u043c\u0430\u0442\u0441\u044b\u0437 \u0431\u0430! \u049a\u0430\u0437\u0430\u049b \u0442\u0456\u043b\u0456\u043d \u0442\u0430\u04a3\u0434\u0430\u0443 \u04af\u0448\u0456\u043d \u0431\u0456\u0440 \u0446\u0438\u0444\u0440\u044b\u043d \u0442\u0435\u0440\u0456\u04a3\u0456\u0437.',
language: 'kz',
},
{
prompt_audio_key: 'ivr/demo-language-ru',
prompt_text:
'\u0417\u0434\u0440\u0430\u0432\u0441\u0442\u0432\u0443\u0439\u0442\u0435! \u0414\u043e\u0431\u0440\u043e \u043f\u043e\u0436\u0430\u043b\u043e\u0432\u0430\u0442\u044c \u0432 \u043a\u043e\u043d\u0442\u0430\u043a\u0442-\u0446\u0435\u043d\u0442\u0440. \u0414\u043b\u044f \u0440\u0443\u0441\u0441\u043a\u043e\u0433\u043e \u044f\u0437\u044b\u043a\u0430 \u043d\u0430\u0436\u043c\u0438\u0442\u0435 \u0446\u0438\u0444\u0440\u0443 \u0434\u0432\u0430.',
language: 'ru',
},
],
is_terminal: false,
invalid_target_node_id: 'root',
no_input_target_node_id: 'root',
options: [
{ digit: '1', target_node_id: 'voice_start_kz' },
{ digit: '2', target_node_id: 'voice_start_ru' },
],
},
{
node_id: 'voice_start_kz',
prompt_text: '\u049a\u043e\u04a3\u044b\u0440\u0430\u0443\u0434\u044b \u049b\u0430\u0437\u0430\u049b \u0442\u0456\u043b\u0456\u043d\u0434\u0435\u0433\u0456 \u0431\u0430\u0441\u0442\u0430\u043f\u049b\u044b voice-\u0441\u0446\u0435\u043d\u0430\u0440\u0438\u0439\u0433\u0435 \u04e9\u0442\u043a\u0456\u0437\u0456\u043f \u0436\u0430\u0442\u044b\u0440\u043c\u044b\u0437.',
is_terminal: true,
outcome_code: 'voice_start_kz',
resolved_queue_id: 'voice_start_kz_line',
resolved_queue_code: 'voice_start_kz',
options: [],
},
{
node_id: 'voice_start_ru',
prompt_text: '\u041f\u0435\u0440\u0435\u0434\u0430\u0435\u043c \u0437\u0432\u043e\u043d\u043e\u043a \u0432 \u0440\u0443\u0441\u0441\u043a\u043e\u044f\u0437\u044b\u0447\u043d\u044b\u0439 \u0441\u0442\u0430\u0440\u0442\u043e\u0432\u044b\u0439 voice-\u0441\u0446\u0435\u043d\u0430\u0440\u0438\u0439.',
is_terminal: true,
outcome_code: 'voice_start_ru',
resolved_queue_id: 'voice_start_ru_line',
resolved_queue_code: 'voice_start_ru',
options: [],
},
],
};
}
function ensureDefaultIvrFlowJson() {
if (!$('ivrFlowJson').value.trim()) {
$('ivrFlowJson').value = JSON.stringify(defaultIvrFlowDocument(), null, 2);
$('ivrFlowJson').value = JSON.stringify(defaultVoiceStartIvrFlowDocument(), null, 2);
}
}
@@ -444,7 +496,7 @@ function syncIvrFlowFields(flow) {
$('ivrQueueId').value = flow.queue_id || $('ivrQueueId').value;
$('ivrEntryNodeId').value = flow.entry_node_id || $('ivrEntryNodeId').value;
$('ivrIsActive').checked = Boolean(flow.is_active);
$('ivrFlowJson').value = JSON.stringify(flow.flow_json || defaultIvrFlowDocument(), null, 2);
$('ivrFlowJson').value = JSON.stringify(flow.flow_json || defaultVoiceStartIvrFlowDocument(), null, 2);
}
function syncIvrSessionFields(payload) {
@@ -458,6 +510,146 @@ function syncIvrSessionFields(payload) {
$('ivrTestInteractionId').value = session.interaction_id || $('ivrTestInteractionId').value;
}
function serializeVoiceNameConfigForm() {
return {
enabled: $('voiceNameEnabled').checked,
start: {
ask_name_on_start: $('voiceNameAskOnStart').checked,
known_customer_behavior: $('voiceNameKnownCustomerBehavior').value,
unknown_customer_behavior: $('voiceNameUnknownCustomerBehavior').value,
},
downstream: {
missing_name_behavior: $('voiceNameMissingNameBehavior').value,
uncertain_name_behavior: $('voiceNameUncertainNameBehavior').value,
finalize_on_explicit_name: $('voiceNameFinalizeOnExplicitName').checked,
finalize_on_confirmation: $('voiceNameFinalizeOnConfirmation').checked,
},
texts: {
ru: {
start_prompt: $('voiceNameRuStartPrompt').value.trim(),
personalized_greeting_template: $('voiceNameRuPersonalizedGreeting').value.trim(),
confirmation_greeting_template: $('voiceNameRuConfirmationGreeting').value.trim(),
inline_followup_prompt: $('voiceNameRuInlineFollowup').value.trim(),
},
kz: {
start_prompt: $('voiceNameKzStartPrompt').value.trim(),
personalized_greeting_template: $('voiceNameKzPersonalizedGreeting').value.trim(),
confirmation_greeting_template: $('voiceNameKzConfirmationGreeting').value.trim(),
inline_followup_prompt: $('voiceNameKzInlineFollowup').value.trim(),
},
},
};
}
function syncVoiceNameConfigFields(payload) {
const config = payload?.config || payload;
if (!config) {
return;
}
$('voiceNameEnabled').checked = Boolean(config.enabled);
$('voiceNameAskOnStart').checked = Boolean(config.start?.ask_name_on_start);
$('voiceNameKnownCustomerBehavior').value = config.start?.known_customer_behavior || 'trust_and_handoff';
$('voiceNameUnknownCustomerBehavior').value = config.start?.unknown_customer_behavior || 'ask_on_start';
$('voiceNameMissingNameBehavior').value = config.downstream?.missing_name_behavior || 'ask_inline_once';
$('voiceNameUncertainNameBehavior').value = config.downstream?.uncertain_name_behavior || 'confirm_then_finalize';
$('voiceNameFinalizeOnExplicitName').checked = Boolean(config.downstream?.finalize_on_explicit_name);
$('voiceNameFinalizeOnConfirmation').checked = Boolean(config.downstream?.finalize_on_confirmation);
$('voiceNameRuStartPrompt').value = config.texts?.ru?.start_prompt || '';
$('voiceNameRuPersonalizedGreeting').value = config.texts?.ru?.personalized_greeting_template || '';
$('voiceNameRuConfirmationGreeting').value = config.texts?.ru?.confirmation_greeting_template || '';
$('voiceNameRuInlineFollowup').value = config.texts?.ru?.inline_followup_prompt || '';
$('voiceNameKzStartPrompt').value = config.texts?.kz?.start_prompt || '';
$('voiceNameKzPersonalizedGreeting').value = config.texts?.kz?.personalized_greeting_template || '';
$('voiceNameKzConfirmationGreeting').value = config.texts?.kz?.confirmation_greeting_template || '';
$('voiceNameKzInlineFollowup').value = config.texts?.kz?.inline_followup_prompt || '';
}
function renderVoiceNameConfigSummary(payload) {
const config = payload?.config || payload;
const source = payload?.source || 'defaults';
const updatedAt = payload?.updated_at || 'не сохранялось';
const knownLabels = {
trust_and_handoff: 'известное имя сразу принимается и передаётся дальше',
confirm_in_downstream: 'известное имя уходит на подтверждение в downstream',
ask_on_start: 'известного клиента всё равно спрашиваем на старте',
};
const unknownLabels = {
ask_on_start: 'неизвестного клиента спрашиваем на старте',
skip_to_downstream: 'неизвестного клиента сразу передаём в downstream',
};
const missingLabels = {
ask_inline_once: 'если имя не получено, AI спросит inline один раз',
do_not_ask: 'если имя не получено, AI inline не спрашивает',
};
const uncertainLabels = {
confirm_then_finalize: 'неуверенное имя нужно подтвердить',
finalize_immediately: 'неуверенное имя считается финальным сразу',
discard_and_collect: 'неуверенное имя сбрасывается и собирается заново',
};
$('voiceNameSummary').textContent = [
`Сценарий: ${config?.enabled ? 'включён' : 'выключен'}`,
`Стартовый вопрос про имя: ${config?.start?.ask_name_on_start ? 'да' : 'нет'}`,
`Известный клиент: ${knownLabels[config?.start?.known_customer_behavior] || config?.start?.known_customer_behavior || 'не задано'}`,
`Неизвестный клиент: ${unknownLabels[config?.start?.unknown_customer_behavior] || config?.start?.unknown_customer_behavior || 'не задано'}`,
`Если имя не получено: ${missingLabels[config?.downstream?.missing_name_behavior] || config?.downstream?.missing_name_behavior || 'не задано'}`,
`Если имя неуверенное: ${uncertainLabels[config?.downstream?.uncertain_name_behavior] || config?.downstream?.uncertain_name_behavior || 'не задано'}`,
`Финализация по явному имени: ${config?.downstream?.finalize_on_explicit_name ? 'да' : 'нет'}`,
`Финализация по подтверждению: ${config?.downstream?.finalize_on_confirmation ? 'да' : 'нет'}`,
`Источник конфигурации: ${source}`,
`Последнее обновление: ${updatedAt}`,
].join('\n');
}
async function loadVoiceNameConfig() {
try {
const data = await api('ai', 'ai/voice/config/name-collection');
state.voiceNameConfig = data;
syncVoiceNameConfigFields(data);
renderVoiceNameConfigSummary(data);
$('voiceNameConfigOutput').textContent = JSON.stringify(data, null, 2);
log('Настройки voice name collection загружены', {
source: data.source,
updated_at: data.updated_at,
});
} catch (err) {
$('voiceNameConfigOutput').textContent = err.message;
log('Не удалось загрузить настройки voice name collection', { error: err.message });
}
}
async function saveVoiceNameConfig() {
try {
const payload = serializeVoiceNameConfigForm();
const data = await api('ai', 'ai/voice/config/name-collection', {
method: 'PUT',
body: JSON.stringify(payload),
});
state.voiceNameConfig = data;
syncVoiceNameConfigFields(data);
renderVoiceNameConfigSummary(data);
$('voiceNameConfigOutput').textContent = JSON.stringify(data, null, 2);
log('Настройки voice name collection сохранены', {
source: data.source,
updated_at: data.updated_at,
});
} catch (err) {
$('voiceNameConfigOutput').textContent = err.message;
log('Не удалось сохранить настройки voice name collection', { error: err.message });
}
}
function resetVoiceNameConfigForm() {
const snapshot = state.voiceNameConfig;
if (!snapshot) {
$('voiceNameConfigOutput').textContent = 'Сначала загрузите текущие настройки voice name collection.';
return;
}
syncVoiceNameConfigFields(snapshot);
renderVoiceNameConfigSummary(snapshot);
$('voiceNameConfigOutput').textContent = JSON.stringify(snapshot, null, 2);
log('Форма voice name collection сброшена к текущим настройкам');
}
function parseIvrFlowJson() {
ensureDefaultIvrFlowJson();
return JSON.parse($('ivrFlowJson').value || '{}');
@@ -980,7 +1172,14 @@ function wire() {
$('loginBtn').addEventListener('click', login);
$('corporateLoginBtn').addEventListener('click', startCorporateLogin);
$('refreshBtn').addEventListener('click', async () => {
await Promise.all([loadUsers(), loadQueues(), loadIvrFlows(), loadAsteriskStatus(), loadAsteriskEvents()]);
await Promise.all([
loadUsers(),
loadQueues(),
loadIvrFlows(),
loadVoiceNameConfig(),
loadAsteriskStatus(),
loadAsteriskEvents(),
]);
log('Данные админ-консоли обновлены');
});
$('loadUsersBtn').addEventListener('click', loadUsers);
@@ -1002,6 +1201,9 @@ function wire() {
$('sendIvrDigitBtn').addEventListener('click', sendIvrDigit);
$('loadIvrSessionBtn').addEventListener('click', loadIvrSession);
$('previewIvrRouteBtn').addEventListener('click', previewIvrRoute);
$('loadVoiceNameConfigBtn').addEventListener('click', loadVoiceNameConfig);
$('saveVoiceNameConfigBtn').addEventListener('click', saveVoiceNameConfig);
$('resetVoiceNameConfigBtn').addEventListener('click', resetVoiceNameConfigForm);
$('loadAsteriskStatusBtn').addEventListener('click', loadAsteriskStatus);
$('loadAsteriskEventsBtn').addEventListener('click', loadAsteriskEvents);
$('loadAsteriskEventDetailBtn').addEventListener('click', loadAsteriskEventDetail);
@@ -1022,7 +1224,16 @@ async function init() {
wire();
ensureDefaultIvrFlowJson();
updateSessionInfo();
await Promise.all([checkGateway(), loadOidcConfig(), loadUsers(), loadQueues(), loadIvrFlows(), loadAsteriskStatus(), loadAsteriskEvents()]);
await Promise.all([
checkGateway(),
loadOidcConfig(),
loadUsers(),
loadQueues(),
loadIvrFlows(),
loadVoiceNameConfig(),
loadAsteriskStatus(),
loadAsteriskEvents(),
]);
log('Админ-консоль готова');
}