- 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>
1418 lines
39 KiB
HTML
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>
|