WellNuo/wellnuo-debug/debug.html
Sergei f94121b848 Update voice call, equipment tracking, and cleanup
- Update WellNuoLite submodule with Julia AI race condition fix
- Add ultravoxService for voice call handling
- Update voice.tsx with improved call flow
- Update equipment tracking in beneficiary details
- Clean up old data files
- Add react-native-base64 type definitions
- Add debug tools

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-01-22 09:34:01 -08:00

1418 lines
39 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>WellNuo Julia AI Debug Console</title>
<style>
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: 'SF Mono', 'Monaco', 'Inconsolata', 'Fira Code', monospace;
background: #0d1117;
color: #c9d1d9;
min-height: 100vh;
padding: 20px;
}
.container {
max-width: 1600px;
margin: 0 auto;
display: flex;
flex-direction: column;
gap: 16px;
min-height: calc(100vh - 40px);
}
.main-content {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px;
flex: 1;
min-height: 0;
}
@media (max-width: 1024px) {
.main-content {
grid-template-columns: 1fr;
}
}
.panel {
background: #161b22;
border: 1px solid #30363d;
border-radius: 8px;
display: flex;
flex-direction: column;
overflow: hidden;
}
.panel-header {
background: #21262d;
padding: 12px 16px;
border-bottom: 1px solid #30363d;
display: flex;
align-items: center;
justify-content: space-between;
}
.panel-title {
font-size: 14px;
font-weight: 600;
color: #f0f6fc;
display: flex;
align-items: center;
gap: 8px;
}
.status-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: #238636;
}
.status-dot.error { background: #f85149; }
.status-dot.warning { background: #d29922; }
.status-dot.idle { background: #8b949e; }
.panel-body {
flex: 1;
overflow-y: auto;
padding: 16px;
}
/* Config Panel */
.config-section {
margin-bottom: 16px;
}
.config-label {
font-size: 12px;
color: #8b949e;
margin-bottom: 6px;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.config-value {
font-size: 13px;
color: #58a6ff;
background: #0d1117;
padding: 8px 12px;
border-radius: 6px;
border: 1px solid #30363d;
}
select, input[type="text"], input[type="password"] {
width: 100%;
padding: 10px 12px;
font-size: 14px;
font-family: inherit;
background: #0d1117;
border: 1px solid #30363d;
border-radius: 6px;
color: #c9d1d9;
outline: none;
}
select:focus, input[type="text"]:focus, input[type="password"]:focus {
border-color: #58a6ff;
}
select:disabled {
opacity: 0.5;
cursor: not-allowed;
}
/* Chat Panel */
.chat-messages {
flex: 1;
overflow-y: auto;
padding: 16px;
display: flex;
flex-direction: column;
gap: 12px;
}
.message {
max-width: 95%;
padding: 14px 18px;
border-radius: 12px;
font-size: 15px;
line-height: 1.6;
white-space: pre-wrap;
word-wrap: break-word;
}
.message.user {
align-self: flex-end;
background: #238636;
color: #fff;
border-bottom-right-radius: 4px;
}
.message.assistant {
align-self: flex-start;
background: #30363d;
color: #f0f6fc;
border-bottom-left-radius: 4px;
}
.message.system {
align-self: center;
background: #1f2937;
color: #8b949e;
font-size: 12px;
padding: 8px 12px;
}
.message-time {
font-size: 10px;
color: rgba(255,255,255,0.5);
margin-top: 4px;
}
.chat-input-container {
padding: 16px;
border-top: 1px solid #30363d;
display: flex;
gap: 8px;
}
.chat-input {
flex: 1;
padding: 12px 16px;
font-size: 14px;
font-family: inherit;
background: #0d1117;
border: 1px solid #30363d;
border-radius: 8px;
color: #c9d1d9;
outline: none;
resize: none;
}
.chat-input:focus {
border-color: #58a6ff;
}
.btn {
padding: 12px 20px;
font-size: 14px;
font-family: inherit;
font-weight: 600;
border: none;
border-radius: 8px;
cursor: pointer;
transition: all 0.2s;
display: flex;
align-items: center;
gap: 6px;
}
.btn-primary {
background: #238636;
color: #fff;
}
.btn-primary:hover {
background: #2ea043;
}
.btn-primary:disabled {
background: #21262d;
color: #8b949e;
cursor: not-allowed;
}
.btn-voice {
background: #1f6feb;
color: #fff;
width: 48px;
height: 48px;
border-radius: 50%;
padding: 0;
justify-content: center;
}
.btn-voice.recording {
background: #f85149;
animation: pulse 1s infinite;
}
@keyframes pulse {
0%, 100% { transform: scale(1); }
50% { transform: scale(1.05); }
}
.btn-danger {
background: #f85149;
color: #fff;
}
.btn-secondary {
background: #21262d;
color: #c9d1d9;
border: 1px solid #30363d;
}
.btn-small {
padding: 6px 12px;
font-size: 12px;
}
/* Logs Panel */
.log-entry {
font-size: 12px;
line-height: 1.6;
padding: 4px 0;
border-bottom: 1px solid #21262d;
word-break: break-all;
}
.log-entry:last-child {
border-bottom: none;
}
.log-time {
color: #8b949e;
margin-right: 8px;
}
.log-level {
padding: 2px 6px;
border-radius: 4px;
font-size: 10px;
font-weight: 600;
margin-right: 8px;
text-transform: uppercase;
}
.log-level.info { background: #1f6feb; color: #fff; }
.log-level.success { background: #238636; color: #fff; }
.log-level.warn { background: #d29922; color: #000; }
.log-level.error { background: #f85149; color: #fff; }
.log-level.request { background: #a371f7; color: #fff; }
.log-level.response { background: #3fb950; color: #000; }
.log-level.stt { background: #f778ba; color: #000; }
.log-level.tts { background: #79c0ff; color: #000; }
.log-message {
color: #c9d1d9;
}
.log-data {
margin-top: 4px;
padding: 8px;
background: #0d1117;
border-radius: 4px;
font-size: 11px;
white-space: pre-wrap;
color: #7ee787;
max-height: 200px;
overflow-y: auto;
}
/* Header */
.header {
grid-column: 1 / -1;
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 20px;
background: #161b22;
border: 1px solid #30363d;
border-radius: 8px;
}
.header h1 {
font-size: 18px;
font-weight: 600;
color: #f0f6fc;
display: flex;
align-items: center;
gap: 10px;
}
.header-badge {
font-size: 11px;
padding: 4px 8px;
background: #f85149;
color: #fff;
border-radius: 4px;
font-weight: 600;
}
/* STT Live Display */
.stt-live {
padding: 12px 16px;
background: #21262d;
border-bottom: 1px solid #30363d;
min-height: 60px;
}
.stt-label {
font-size: 11px;
color: #8b949e;
text-transform: uppercase;
margin-bottom: 4px;
display: flex;
align-items: center;
gap: 6px;
}
.stt-label .recording-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: #f85149;
animation: blink 1s infinite;
}
@keyframes blink {
0%, 100% { opacity: 1; }
50% { opacity: 0.3; }
}
.stt-text {
font-size: 14px;
color: #f0f6fc;
min-height: 24px;
}
.stt-text.interim {
color: #8b949e;
font-style: italic;
}
/* TTS indicator */
.tts-indicator {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 12px;
background: #21262d;
border-radius: 6px;
font-size: 12px;
}
.tts-indicator.speaking {
background: #238636;
color: #fff;
}
.speaking-animation {
display: flex;
gap: 2px;
}
.speaking-animation span {
width: 3px;
height: 12px;
background: currentColor;
border-radius: 2px;
animation: speak 0.5s infinite alternate;
}
.speaking-animation span:nth-child(2) { animation-delay: 0.1s; }
.speaking-animation span:nth-child(3) { animation-delay: 0.2s; }
@keyframes speak {
from { height: 4px; }
to { height: 12px; }
}
/* Config Grid */
.config-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 12px;
}
.config-grid.full {
grid-template-columns: 1fr;
}
.deployment-list {
margin-top: 8px;
padding: 8px;
background: #0d1117;
border: 1px solid #30363d;
border-radius: 6px;
font-size: 12px;
max-height: 120px;
overflow-y: auto;
}
.deployment-item {
padding: 4px 0;
display: flex;
justify-content: space-between;
border-bottom: 1px solid #21262d;
}
.deployment-item:last-child {
border-bottom: none;
}
.deployment-id {
color: #58a6ff;
font-weight: 600;
}
.deployment-name {
color: #c9d1d9;
}
/* Scrollbar */
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: #161b22;
}
::-webkit-scrollbar-thumb {
background: #30363d;
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: #484f58;
}
.loading {
color: #8b949e;
font-style: italic;
}
/* Collapsible sections */
.collapsible-header {
cursor: pointer;
user-select: none;
display: flex;
align-items: center;
gap: 8px;
}
.collapsible-header:hover {
background: #2d333b;
}
.collapse-icon {
transition: transform 0.2s;
font-size: 12px;
}
.collapsed .collapse-icon {
transform: rotate(-90deg);
}
.collapsible-content {
overflow: hidden;
transition: max-height 0.3s ease;
}
.collapsed .collapsible-content {
max-height: 0 !important;
padding: 0 !important;
}
/* Chat panel takes more space */
.chat-panel {
display: flex;
flex-direction: column;
min-height: 500px;
}
.chat-panel .chat-messages {
flex: 1;
min-height: 300px;
}
/* Config section collapsible */
.config-wrapper {
border-bottom: 1px solid #30363d;
}
.config-wrapper.collapsed {
border-bottom: none;
}
.config-content {
padding: 16px;
max-height: 500px;
}
/* Logs panel improvements */
.logs-panel {
display: flex;
flex-direction: column;
}
.logs-panel .panel-body {
flex: 1;
min-height: 200px;
}
/* Top config bar */
.top-config-bar {
background: #161b22;
border: 1px solid #30363d;
border-radius: 8px;
padding: 12px 16px;
display: flex;
gap: 16px;
align-items: center;
flex-wrap: wrap;
}
.top-config-bar .config-section {
margin-bottom: 0;
flex: 1;
min-width: 150px;
}
.top-config-bar .config-label {
margin-bottom: 4px;
}
</style>
</head>
<body>
<div class="container">
<!-- Header -->
<div class="header">
<h1>
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#58a6ff" stroke-width="2">
<path d="M12 2a3 3 0 0 0-3 3v7a3 3 0 0 0 6 0V5a3 3 0 0 0-3-3Z"/>
<path d="M19 10v2a7 7 0 0 1-14 0v-2"/>
<line x1="12" x2="12" y1="19" y2="22"/>
</svg>
WellNuo Julia AI Debug Console
<span class="header-badge">DEV</span>
</h1>
<div class="tts-indicator" id="ttsIndicator">
<span>TTS:</span>
<span id="ttsStatus">Idle</span>
</div>
</div>
<!-- Top Config Bar -->
<div class="top-config-bar">
<div class="config-section">
<div class="config-label">Username</div>
<select id="userSelect" onchange="loadUserDeployments()">
<option value="anandk">anandk</option>
<option value="custom">Custom...</option>
</select>
</div>
<div class="config-section" id="passwordSection">
<div class="config-label">Password</div>
<input type="password" id="passwordInput" value="anandk_8" />
</div>
<div class="config-section" style="flex: 2;">
<div class="config-label">Deployment (Beneficiary)</div>
<select id="deploymentSelect" onchange="updateDeployment()" disabled>
<option value="">Loading deployments...</option>
</select>
</div>
<div class="config-section">
<div class="config-label">Status</div>
<div style="display: flex; align-items: center; gap: 8px; padding: 8px 0;">
<div class="status-dot idle" id="connectionStatus"></div>
<span id="connectionText" style="font-size: 13px;">Not connected</span>
</div>
</div>
</div>
<!-- Main Content: Chat + Logs -->
<div class="main-content">
<!-- Left Panel: Chat -->
<div class="panel chat-panel">
<div class="panel-header">
<div class="panel-title">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/>
</svg>
<span>Chat with Julia AI</span>
</div>
<button class="btn btn-secondary btn-small" onclick="clearChat()">Clear Chat</button>
</div>
<!-- Collapsible Deployment Details -->
<div class="config-wrapper" id="deploymentDetailsWrapper">
<div class="panel-header collapsible-header" onclick="toggleSection('deploymentDetails')" style="padding: 8px 16px; background: #1a1f26;">
<span class="collapse-icon"></span>
<span style="font-size: 12px; color: #8b949e;">Deployment Details & Names Dict</span>
</div>
<div class="collapsible-content config-content" id="deploymentDetails" style="padding: 12px 16px;">
<div class="deployment-list" id="deploymentList" style="max-height: 80px; margin-bottom: 12px;">
<span class="loading">Select a user to load deployments...</span>
</div>
<div class="config-label">Beneficiary Names Dict (JSON)</div>
<input type="text" id="namesDict" value='{}' style="font-size: 12px;" />
</div>
</div>
<!-- STT Live Display -->
<div class="stt-live" id="sttLive" style="display: none;">
<div class="stt-label">
<span class="recording-dot"></span>
Speech Recognition (STT)
</div>
<div class="stt-text" id="sttText">Listening...</div>
</div>
<!-- Chat Messages -->
<div class="chat-messages" id="chatMessages">
<div class="message system">
Welcome to Julia AI Debug Console. Select a user, then a deployment, and start chatting!
</div>
</div>
<!-- Chat Input -->
<div class="chat-input-container">
<input type="text" class="chat-input" id="chatInput" placeholder="Type a message or press mic button..." onkeypress="handleKeyPress(event)">
<button class="btn btn-voice" id="voiceBtn" onmousedown="startVoice()" onmouseup="stopVoice()" ontouchstart="startVoice()" ontouchend="stopVoice()">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M12 2a3 3 0 0 0-3 3v7a3 3 0 0 0 6 0V5a3 3 0 0 0-3-3Z"/>
<path d="M19 10v2a7 7 0 0 1-14 0v-2"/>
<line x1="12" x2="12" y1="19" y2="22"/>
</svg>
</button>
<button class="btn btn-primary" onclick="sendMessage()" id="sendBtn">Send</button>
</div>
</div>
<!-- Right Panel: Logs -->
<div class="panel logs-panel">
<div class="panel-header collapsible-header" onclick="toggleSection('logsContent')">
<div class="panel-title">
<span class="collapse-icon"></span>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/>
<polyline points="14 2 14 8 20 8"/>
<line x1="16" y1="13" x2="8" y2="13"/>
<line x1="16" y1="17" x2="8" y2="17"/>
<polyline points="10 9 9 9 8 9"/>
</svg>
<span>Full Request/Response Logs</span>
</div>
<button class="btn btn-secondary btn-small" onclick="event.stopPropagation(); clearLogs()">Clear</button>
</div>
<div class="collapsible-content panel-body" id="logsContent">
<div id="logsContainer">
<!-- Logs will appear here -->
</div>
</div>
</div>
</div>
</div>
<script>
// =====================
// CONFIGURATION
// =====================
const WELLNUO_API_URL = 'https://eluxnetworks.net/function/well-api/api';
// Known users with passwords
const KNOWN_USERS = {
'anandk': 'anandk_8'
};
let authToken = null;
let currentUser = 'anandk';
let currentPassword = 'anandk_8';
let currentDeploymentId = null;
let deployments = [];
let isRecording = false; // STT is actively listening
let wantsRecording = false; // User wants to record (button pressed)
let isTTSPlaying = false; // TTS is currently playing
let recognition = null;
let synthesis = window.speechSynthesis;
// =====================
// LOGGING
// =====================
function log(level, message, data = null) {
const now = new Date();
const time = now.toLocaleTimeString('en-US', {
hour12: false,
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
}) + '.' + now.getMilliseconds().toString().padStart(3, '0');
const logsContainer = document.getElementById('logsContainer');
const entry = document.createElement('div');
entry.className = 'log-entry';
let html = `
<span class="log-time">${time}</span>
<span class="log-level ${level}">${level}</span>
<span class="log-message">${escapeHtml(message)}</span>
`;
if (data) {
const formatted = typeof data === 'string' ? data : JSON.stringify(data, null, 2);
html += `<div class="log-data">${escapeHtml(formatted)}</div>`;
}
entry.innerHTML = html;
logsContainer.appendChild(entry);
logsContainer.scrollTop = logsContainer.scrollHeight;
// Also console log
console.log(`[${level.toUpperCase()}] ${message}`, data || '');
}
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
function clearLogs() {
document.getElementById('logsContainer').innerHTML = '';
log('info', 'Logs cleared');
}
function toggleSection(sectionId) {
const content = document.getElementById(sectionId);
const wrapper = content.closest('.config-wrapper') || content.closest('.panel');
if (wrapper) {
wrapper.classList.toggle('collapsed');
}
}
// =====================
// AUTHENTICATION
// =====================
async function authenticate() {
currentUser = document.getElementById('userSelect').value === 'custom'
? document.getElementById('customUser')?.value || 'anandk'
: document.getElementById('userSelect').value;
currentPassword = document.getElementById('passwordInput').value;
log('request', `Authenticating as "${currentUser}"...`, {
url: WELLNUO_API_URL,
user: currentUser,
function: 'credentials'
});
setConnectionStatus('warning');
const startTime = Date.now();
try {
const nonce = Math.floor(Math.random() * 1000000).toString();
// IMPORTANT: API requires application/x-www-form-urlencoded, NOT FormData!
const params = new URLSearchParams();
params.append('function', 'credentials');
params.append('clientId', 'MA_001');
params.append('user_name', currentUser);
params.append('ps', currentPassword);
params.append('nonce', nonce);
const response = await fetch(WELLNUO_API_URL, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
body: params.toString()
});
const elapsed = Date.now() - startTime;
const result = await response.json();
log('response', `Auth response (${elapsed}ms)`, result);
if (result.status === '200 OK' && result.access_token) {
authToken = result.access_token;
log('success', `Authenticated! User ID: ${result.user_id}, Max Role: ${result.max_role}`);
log('info', `Privileges: ${result.privileges}`);
setConnectionStatus('success');
return true;
} else {
log('error', 'Authentication failed', result);
setConnectionStatus('error');
return false;
}
} catch (error) {
log('error', 'Authentication error', { message: error.message, stack: error.stack });
setConnectionStatus('error');
return false;
}
}
// =====================
// LOAD DEPLOYMENTS
// =====================
async function loadUserDeployments() {
const userSelect = document.getElementById('userSelect');
const deploymentSelect = document.getElementById('deploymentSelect');
const deploymentList = document.getElementById('deploymentList');
// Update password if known user
if (KNOWN_USERS[userSelect.value]) {
document.getElementById('passwordInput').value = KNOWN_USERS[userSelect.value];
}
deploymentSelect.disabled = true;
deploymentSelect.innerHTML = '<option value="">Loading deployments...</option>';
deploymentList.innerHTML = '<span class="loading">Loading...</span>';
// Authenticate first
authToken = null;
const authSuccess = await authenticate();
if (!authSuccess) {
deploymentSelect.innerHTML = '<option value="">Authentication failed</option>';
deploymentList.innerHTML = '<span class="loading">Authentication failed. Check credentials.</span>';
return;
}
// Load deployments
log('request', `Loading deployments for "${currentUser}"...`, {
function: 'deployments_list',
user: currentUser
});
const startTime = Date.now();
try {
const params = new URLSearchParams();
params.append('function', 'deployments_list');
params.append('user_name', currentUser);
params.append('token', authToken);
params.append('first', '0');
params.append('last', '100');
const response = await fetch(WELLNUO_API_URL, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
body: params.toString()
});
const elapsed = Date.now() - startTime;
const result = await response.json();
log('response', `Deployments response (${elapsed}ms)`, result);
if (result.status === '200 OK' && result.result_list) {
deployments = result.result_list;
// Update select
deploymentSelect.innerHTML = deployments.map(d =>
`<option value="${d.deployment_id}">${d.deployment_id} - ${d.first_name} ${d.last_name}</option>`
).join('');
deploymentSelect.disabled = false;
// Update list display
deploymentList.innerHTML = deployments.map(d => `
<div class="deployment-item">
<span class="deployment-id">#${d.deployment_id}</span>
<span class="deployment-name">${d.first_name} ${d.last_name}</span>
</div>
`).join('');
// Build beneficiary names dict
const namesDict = {};
deployments.forEach(d => {
namesDict[d.deployment_id] = d.first_name.toLowerCase();
});
document.getElementById('namesDict').value = JSON.stringify(namesDict);
// Set current deployment
if (deployments.length > 0) {
currentDeploymentId = deployments[0].deployment_id;
log('success', `Loaded ${deployments.length} deployments. Selected: ${currentDeploymentId}`);
}
addMessage('system', `Loaded ${deployments.length} deployments for ${currentUser}. Ready to chat!`);
} else {
log('error', 'Failed to load deployments', result);
deploymentSelect.innerHTML = '<option value="">No deployments found</option>';
deploymentList.innerHTML = '<span class="loading">No deployments found</span>';
}
} catch (error) {
log('error', 'Load deployments error', { message: error.message });
deploymentSelect.innerHTML = '<option value="">Error loading</option>';
deploymentList.innerHTML = '<span class="loading">Error loading deployments</span>';
}
}
function updateDeployment() {
currentDeploymentId = document.getElementById('deploymentSelect').value;
const deployment = deployments.find(d => d.deployment_id == currentDeploymentId);
if (deployment) {
log('info', `Selected deployment: ${currentDeploymentId} (${deployment.first_name} ${deployment.last_name})`);
}
}
// =====================
// ASK WELLNUO AI
// =====================
async function askWellNuoAI(question) {
if (!authToken) {
log('warn', 'No auth token, authenticating first...');
const success = await authenticate();
if (!success) {
return 'Authentication failed. Please try again.';
}
}
if (!currentDeploymentId) {
log('error', 'No deployment selected!');
return 'Please select a deployment first.';
}
let beneficiaryNamesDict = {};
try {
beneficiaryNamesDict = JSON.parse(document.getElementById('namesDict').value);
} catch (e) {
log('warn', 'Invalid beneficiary_names_dict JSON, using empty object');
}
const requestData = {
function: 'ask_wellnuo_ai',
clientId: 'MA_001',
user_name: currentUser,
token: authToken,
question: question,
deployment_id: currentDeploymentId,
beneficiary_names_dict: JSON.stringify(beneficiaryNamesDict)
};
log('request', `Sending question to WellNuo AI`, {
url: WELLNUO_API_URL,
user: currentUser,
deployment_id: currentDeploymentId,
question: question,
beneficiary_names_dict: beneficiaryNamesDict
});
const startTime = Date.now();
try {
const params = new URLSearchParams();
Object.entries(requestData).forEach(([key, value]) => {
params.append(key, value);
});
const response = await fetch(WELLNUO_API_URL, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
body: params.toString()
});
const elapsed = Date.now() - startTime;
const result = await response.json();
log('response', `WellNuo AI response (${elapsed}ms)`, result);
if (result.ok && result.response && result.response.body) {
const answer = result.response.body;
log('success', `Answer: "${answer.substring(0, 100)}${answer.length > 100 ? '...' : ''}"`);
return answer;
} else {
log('error', 'No valid response from WellNuo AI', result);
// Check if token expired
if (result.message && result.message.includes('token')) {
log('warn', 'Token expired, re-authenticating...');
authToken = null;
return await askWellNuoAI(question);
}
return result.message || 'Sorry, I could not process your request.';
}
} catch (error) {
log('error', 'API request error', { message: error.message, stack: error.stack });
return 'Connection error. Please try again.';
}
}
// =====================
// CHAT UI
// =====================
function addMessage(role, text) {
const chatMessages = document.getElementById('chatMessages');
const message = document.createElement('div');
message.className = `message ${role}`;
const time = new Date().toLocaleTimeString('en-US', {
hour12: false,
hour: '2-digit',
minute: '2-digit'
});
message.innerHTML = `
${escapeHtml(text)}
<div class="message-time">${time}</div>
`;
chatMessages.appendChild(message);
chatMessages.scrollTop = chatMessages.scrollHeight;
}
function clearChat() {
const chatMessages = document.getElementById('chatMessages');
chatMessages.innerHTML = '<div class="message system">Chat cleared. Start a new conversation!</div>';
log('info', 'Chat cleared');
}
async function sendMessage() {
const input = document.getElementById('chatInput');
const text = input.value.trim();
if (!text) return;
input.value = '';
input.disabled = true;
document.getElementById('sendBtn').disabled = true;
log('info', `User message: "${text}"`);
addMessage('user', text);
// Get response
const response = await askWellNuoAI(text);
addMessage('assistant', response);
// Speak the response
speak(response);
input.disabled = false;
document.getElementById('sendBtn').disabled = false;
input.focus();
}
function handleKeyPress(event) {
if (event.key === 'Enter' && !event.shiftKey) {
event.preventDefault();
sendMessage();
}
}
// =====================
// VOICE RECOGNITION (STT)
// =====================
function initSpeechRecognition() {
if (!('webkitSpeechRecognition' in window) && !('SpeechRecognition' in window)) {
log('warn', 'Speech recognition not supported in this browser');
document.getElementById('voiceBtn').style.display = 'none';
return;
}
const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
recognition = new SpeechRecognition();
recognition.continuous = true;
recognition.interimResults = true;
recognition.lang = 'en-US';
recognition.onstart = () => {
log('stt', 'Speech recognition STARTED');
isRecording = true;
document.getElementById('voiceBtn').classList.add('recording');
document.getElementById('sttLive').style.display = 'block';
document.getElementById('sttText').textContent = 'Listening...';
document.getElementById('sttText').className = 'stt-text interim';
};
recognition.onresult = (event) => {
let interimTranscript = '';
let finalTranscript = '';
for (let i = event.resultIndex; i < event.results.length; i++) {
const transcript = event.results[i][0].transcript;
const confidence = event.results[i][0].confidence;
if (event.results[i].isFinal) {
finalTranscript += transcript;
log('stt', `FINAL: "${transcript}" (confidence: ${(confidence * 100).toFixed(1)}%)`);
} else {
interimTranscript += transcript;
}
}
// Update STT display
const sttText = document.getElementById('sttText');
if (interimTranscript) {
sttText.textContent = interimTranscript;
sttText.className = 'stt-text interim';
log('stt', `Interim: "${interimTranscript}"`);
}
if (finalTranscript) {
sttText.textContent = finalTranscript;
sttText.className = 'stt-text';
document.getElementById('chatInput').value = finalTranscript;
}
};
recognition.onerror = (event) => {
log('error', `STT error: ${event.error}`, {
error: event.error,
message: event.message
});
stopVoice();
};
recognition.onend = () => {
log('stt', 'Speech recognition ENDED');
isRecording = false;
// DON'T auto-restart if TTS is playing (to avoid echo)
if (isTTSPlaying) {
log('stt', 'TTS is playing, NOT restarting STT');
return;
}
// Auto-restart if user still wants recording
if (wantsRecording) {
log('stt', 'Auto-restarting recognition...');
setTimeout(() => {
if (wantsRecording && !isTTSPlaying) {
try {
recognition.start();
isRecording = true;
} catch (e) {
log('warn', 'Could not auto-restart: ' + e.message);
stopVoice(false);
}
}
}, 100);
}
};
recognition.onspeechstart = () => {
log('stt', 'Speech detected');
};
recognition.onspeechend = () => {
log('stt', 'Speech ended');
};
log('success', 'Speech recognition initialized (Web Speech API)');
}
async function startVoice() {
if (wantsRecording) return;
// Stop any ongoing TTS first
synthesis.cancel();
isTTSPlaying = false;
setTTSStatus('Idle');
// Request microphone permission first
try {
log('stt', 'Requesting microphone permission...');
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
log('stt', 'Microphone access granted');
// Stop the stream - we just needed permission
stream.getTracks().forEach(track => track.stop());
} catch (e) {
log('error', 'Microphone access denied', { message: e.message });
alert('Please allow microphone access to use voice input');
return;
}
wantsRecording = true; // User wants to record
try {
isRecording = true;
recognition.start();
log('stt', 'Starting voice recognition...');
document.getElementById('voiceBtn').classList.add('recording');
document.getElementById('sttLive').style.display = 'block';
} catch (e) {
isRecording = false;
wantsRecording = false;
log('error', 'Failed to start recognition', { message: e.message });
}
}
function stopVoice(autoSend = true) {
wantsRecording = false; // User no longer wants recording
try {
recognition.stop();
} catch (e) {
// Ignore
}
isRecording = false;
document.getElementById('voiceBtn').classList.remove('recording');
document.getElementById('sttLive').style.display = 'none';
// Auto-send if we have text (only if autoSend is true)
if (autoSend) {
const input = document.getElementById('chatInput');
if (input.value.trim()) {
log('info', 'Voice input complete, sending message...');
setTimeout(() => sendMessage(), 300);
}
}
}
// =====================
// TEXT-TO-SPEECH (TTS)
// =====================
function speak(text) {
if (!synthesis) {
log('warn', 'Speech synthesis not supported');
return;
}
// Cancel any ongoing speech
synthesis.cancel();
log('tts', `Speaking: "${text.substring(0, 80)}${text.length > 80 ? '...' : ''}"`);
const utterance = new SpeechSynthesisUtterance(text);
utterance.rate = 1.0;
utterance.pitch = 1.0;
utterance.volume = 1.0;
// Try to use a female voice (Julia-like)
const voices = synthesis.getVoices();
const femaleVoice = voices.find(v =>
v.name.includes('Samantha') ||
v.name.includes('Victoria') ||
v.name.includes('Karen') ||
v.name.includes('Zira') ||
(v.name.includes('Female') && v.lang.startsWith('en'))
) || voices.find(v => v.lang.startsWith('en'));
if (femaleVoice) {
utterance.voice = femaleVoice;
log('tts', `Using voice: "${femaleVoice.name}" (${femaleVoice.lang})`);
}
utterance.onstart = () => {
log('tts', 'TTS Started');
isTTSPlaying = true;
setTTSStatus('Speaking', true);
// STOP STT completely while TTS is playing to avoid echo
if (recognition) {
log('stt', 'Stopping STT during TTS playback...');
try { recognition.stop(); } catch(e) {}
isRecording = false;
}
};
utterance.onend = () => {
log('tts', 'TTS Finished');
isTTSPlaying = false;
setTTSStatus('Idle');
// Resume STT after TTS finishes ONLY if user still wants recording
if (wantsRecording) {
log('stt', 'Resuming STT after TTS...');
setTimeout(() => {
if (wantsRecording && !isTTSPlaying) {
try {
recognition.start();
isRecording = true;
} catch(e) {
log('warn', 'Could not resume STT: ' + e.message);
}
}
}, 500);
}
};
utterance.onerror = (event) => {
log('error', `TTS error: ${event.error}`, event);
setTTSStatus('Error');
};
utterance.onpause = () => log('tts', 'TTS Paused');
utterance.onresume = () => log('tts', 'TTS Resumed');
synthesis.speak(utterance);
}
function setTTSStatus(status, speaking = false) {
const indicator = document.getElementById('ttsIndicator');
const statusEl = document.getElementById('ttsStatus');
if (speaking) {
indicator.classList.add('speaking');
statusEl.innerHTML = `<div class="speaking-animation"><span></span><span></span><span></span></div> ${status}`;
} else {
indicator.classList.remove('speaking');
statusEl.textContent = status;
}
}
// =====================
// CONNECTION STATUS
// =====================
function setConnectionStatus(status) {
const dot = document.getElementById('connectionStatus');
dot.className = 'status-dot';
switch (status) {
case 'success':
// green by default
break;
case 'warning':
dot.classList.add('warning');
break;
case 'error':
dot.classList.add('error');
break;
default:
dot.classList.add('idle');
}
}
// =====================
// INIT
// =====================
window.onload = async () => {
log('info', '=== WellNuo Julia AI Debug Console ===');
log('info', `API URL: ${WELLNUO_API_URL}`);
log('info', 'Browser: ' + navigator.userAgent.substring(0, 80) + '...');
// Init speech recognition
initSpeechRecognition();
// Load voices for TTS
if (synthesis) {
const loadVoices = () => {
const voices = synthesis.getVoices();
if (voices.length > 0) {
log('info', `TTS: ${voices.length} voices available`);
const englishVoices = voices.filter(v => v.lang.startsWith('en'));
log('info', `TTS English voices: ${englishVoices.map(v => v.name).join(', ')}`);
}
};
synthesis.onvoiceschanged = loadVoices;
loadVoices();
}
// Load deployments for default user
await loadUserDeployments();
log('success', 'Debug console ready!');
};
</script>
</body>
</html>