feat(chat-turnero): rich messaging UI — images, video, audio, reactions, emoji, templates
- Attach button with file preview (image thumb for images, name+size for others) - Audio recording via MediaRecorder API with live timer; sends as voice note - Emoji panel with 35 common emojis (medical/comms focused) - Reaction picker on message hover — sends via chat_react.php API - Template modal (name, lang, JSON params) — sends via chat_send_message.php type=template - buildBubble() now renders: inline images, <video> player, <audio> player, doc links with type icon - Shows reaction_emoji below bubble and status tick on outgoing messages Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
3b378c0d75
commit
a7ab91615f
+401
-14
@@ -167,6 +167,90 @@ html, body { height: 100%; overflow: hidden; font-family: -apple-system, BlinkMa
|
||||
}
|
||||
.typing-indicator { font-size: .75rem; color: #888; padding: 2px 8px; }
|
||||
|
||||
/* ── Botones extra en input ── */
|
||||
.input-extra-btn {
|
||||
background: none; border: none; color: #888; font-size: 1.15rem;
|
||||
cursor: pointer; padding: 6px; border-radius: 50%; transition: background .15s, color .15s;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.input-extra-btn:hover { background: rgba(0,0,0,.06); color: #555; }
|
||||
|
||||
/* ── Panel emoji ── */
|
||||
.emoji-panel {
|
||||
position: absolute; bottom: 64px; left: 0;
|
||||
background: #fff; border: 1px solid #e0e0e0; border-radius: 12px;
|
||||
box-shadow: 0 6px 24px rgba(0,0,0,.15); padding: 10px;
|
||||
display: none; flex-wrap: wrap; gap: 2px; width: 280px; z-index: 50;
|
||||
}
|
||||
.emoji-panel.show { display: flex; }
|
||||
.emoji-panel span { font-size: 1.3rem; cursor: pointer; padding: 4px; border-radius: 6px; }
|
||||
.emoji-panel span:hover { background: #f0f2f5; }
|
||||
|
||||
/* ── Reaction picker ── */
|
||||
#reaction-picker {
|
||||
position: fixed; display: none; z-index: 9000;
|
||||
background: #fff; border-radius: 30px; padding: 6px 10px;
|
||||
box-shadow: 0 4px 20px rgba(0,0,0,.18);
|
||||
gap: 4px; align-items: center;
|
||||
}
|
||||
#reaction-picker.show { display: flex; }
|
||||
#reaction-picker span { font-size: 1.4rem; cursor: pointer; padding: 4px 6px; border-radius: 50%; transition: transform .1s; }
|
||||
#reaction-picker span:hover { transform: scale(1.3); }
|
||||
.msg-bubble { position: relative; }
|
||||
.msg-bubble:hover .react-btn { opacity: 1; }
|
||||
.react-btn {
|
||||
position: absolute; top: 4px; opacity: 0; transition: opacity .15s;
|
||||
background: #fff; border: 1px solid #e0e0e0; border-radius: 50%;
|
||||
width: 24px; height: 24px; font-size: .75rem; cursor: pointer;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
box-shadow: 0 1px 4px rgba(0,0,0,.12);
|
||||
}
|
||||
.msg-bubble.incoming .react-btn { right: -28px; }
|
||||
.msg-bubble.outgoing .react-btn { left: -28px; }
|
||||
.msg-reaction { font-size: .95rem; margin-top: 2px; }
|
||||
|
||||
/* ── Preview adjunto ── */
|
||||
#attach-preview {
|
||||
display: none; align-items: center; gap: 8px;
|
||||
padding: 8px 12px; background: #e9f5fe;
|
||||
border-top: 1px solid #bee3f8; font-size: .85rem;
|
||||
}
|
||||
#attach-preview img { width: 48px; height: 48px; object-fit: cover; border-radius: 6px; }
|
||||
#attach-preview .attach-name { flex: 1; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
#attach-preview .attach-remove { background: none; border: none; color: #e53e3e; cursor: pointer; font-size: 1rem; }
|
||||
|
||||
/* ── Recording indicator ── */
|
||||
#recording-bar {
|
||||
display: none; align-items: center; gap: 10px;
|
||||
padding: 8px 14px; background: #fff0f0; border-top: 1px solid #fed7d7;
|
||||
font-size: .85rem; color: #e53e3e;
|
||||
}
|
||||
#recording-bar .rec-dot { width: 10px; height: 10px; border-radius: 50%; background: #e53e3e; animation: blink 1s infinite; }
|
||||
@keyframes blink { 0%,100%{opacity:1} 50%{opacity:.2} }
|
||||
|
||||
/* ── Modal plantilla ── */
|
||||
#template-modal {
|
||||
display: none; position: fixed; inset: 0; z-index: 1000;
|
||||
background: rgba(0,0,0,.4); align-items: center; justify-content: center;
|
||||
}
|
||||
#template-modal.show { display: flex; }
|
||||
.tpl-card { background: #fff; border-radius: 14px; padding: 24px; width: 340px; box-shadow: 0 8px 32px rgba(0,0,0,.2); }
|
||||
.tpl-card h3 { font-size: 1rem; font-weight: 700; margin-bottom: 16px; }
|
||||
.tpl-field { margin-bottom: 12px; }
|
||||
.tpl-field label { font-size: .8rem; color: #555; font-weight: 600; display: block; margin-bottom: 4px; }
|
||||
.tpl-field input { width: 100%; padding: 8px 10px; border: 1.5px solid #e0e0e0; border-radius: 8px; font-size: .9rem; outline: none; }
|
||||
.tpl-field input:focus { border-color: #25d366; }
|
||||
.tpl-actions { display: flex; gap: 8px; justify-content: flex-end; margin-top: 4px; }
|
||||
.btn-tpl-cancel { padding: 8px 16px; background: #f5f5f5; border: none; border-radius: 8px; cursor: pointer; font-size: .88rem; }
|
||||
.btn-tpl-send { padding: 8px 16px; background: #25d366; color: #fff; border: none; border-radius: 8px; cursor: pointer; font-size: .88rem; font-weight: 600; }
|
||||
|
||||
/* ── Media en burbuja ── */
|
||||
.msg-img { max-width: 220px; max-height: 200px; border-radius: 8px; cursor: pointer; display: block; margin-bottom: 3px; }
|
||||
.msg-video { max-width: 220px; border-radius: 8px; display: block; margin-bottom: 3px; }
|
||||
.msg-audio { width: 200px; margin-bottom: 3px; }
|
||||
.msg-doc-link { display: flex; align-items: center; gap: 7px; font-size: .83rem; color: #1a73e8; text-decoration: none; padding: 4px 0; }
|
||||
.msg-doc-link:hover { text-decoration: underline; }
|
||||
|
||||
.back-btn { display: none; background: none; border: none; color: #fff; font-size: 1.1rem; cursor: pointer; padding: 4px 8px; }
|
||||
|
||||
@media (max-width: 680px) {
|
||||
@@ -243,9 +327,34 @@ html, body { height: 100%; overflow: hidden; font-family: -apple-system, BlinkMa
|
||||
|
||||
<div id="typingIndicator" class="typing-indicator" style="display:none">Escribiendo…</div>
|
||||
|
||||
<div class="chat-input-area">
|
||||
<!-- Preview adjunto -->
|
||||
<div id="attach-preview">
|
||||
<img id="attachThumb" src="" alt="" style="display:none">
|
||||
<span class="attach-name" id="attachName"></span>
|
||||
<button class="attach-remove" onclick="clearAttach()">✕</button>
|
||||
</div>
|
||||
|
||||
<!-- Barra de grabación -->
|
||||
<div id="recording-bar">
|
||||
<div class="rec-dot"></div>
|
||||
<span>Grabando…</span>
|
||||
<span id="recTimer" style="font-weight:700;min-width:32px">0:00</span>
|
||||
<button onclick="stopRecording()" style="margin-left:auto;background:#e53e3e;color:#fff;border:none;border-radius:6px;padding:4px 12px;cursor:pointer;font-size:.82rem">Enviar</button>
|
||||
<button onclick="cancelRecording()" style="background:#f5f5f5;border:none;border-radius:6px;padding:4px 10px;cursor:pointer;font-size:.82rem">Cancelar</button>
|
||||
</div>
|
||||
|
||||
<input type="file" id="fileInput" style="display:none"
|
||||
accept="image/*,video/*,audio/*,.pdf,.doc,.docx,.xls,.xlsx,.csv"
|
||||
onchange="onFileSelected(event)">
|
||||
|
||||
<div class="chat-input-area" style="position:relative">
|
||||
<div class="emoji-panel" id="emojiPanel"></div>
|
||||
<button class="input-extra-btn" onclick="toggleEmojiPanel()" title="Emoji"><i class="fas fa-smile"></i></button>
|
||||
<button class="input-extra-btn" onclick="document.getElementById('fileInput').click()" title="Adjuntar"><i class="fas fa-paperclip"></i></button>
|
||||
<button class="input-extra-btn" id="micBtn" onclick="toggleRecording()" title="Grabar audio"><i class="fas fa-microphone"></i></button>
|
||||
<textarea id="msgInput" rows="1" placeholder="Escribe un mensaje…"
|
||||
onkeydown="handleKey(event)" oninput="autoResize(this)"></textarea>
|
||||
<button class="input-extra-btn" onclick="openTemplatePicker()" title="Plantilla"><i class="fas fa-file-alt"></i></button>
|
||||
<button class="btn-send" id="btnSend" onclick="sendMessage()" <?= $turneroPhoneId ? '' : 'disabled' ?>>
|
||||
<i class="fas fa-paper-plane"></i>
|
||||
</button>
|
||||
@@ -255,11 +364,43 @@ html, body { height: 100%; overflow: hidden; font-family: -apple-system, BlinkMa
|
||||
</div><!-- /chat-window -->
|
||||
</div><!-- /chat-shell -->
|
||||
|
||||
<!-- ── Reaction picker ── -->
|
||||
<div id="reaction-picker">
|
||||
<?php foreach (['👍','❤️','😂','😮','😢','🙏','🔥','👏'] as $em): ?>
|
||||
<span onclick="sendReactionEmoji('<?= $em ?>')"><?= $em ?></span>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
|
||||
<!-- ── Modal plantilla ── -->
|
||||
<div id="template-modal">
|
||||
<div class="tpl-card">
|
||||
<h3><i class="fas fa-file-alt" style="color:#25d366;margin-right:6px"></i>Enviar Plantilla</h3>
|
||||
<div class="tpl-field">
|
||||
<label>Nombre de plantilla</label>
|
||||
<input type="text" id="tplName" placeholder="ej: consentimiento_turno">
|
||||
</div>
|
||||
<div class="tpl-field">
|
||||
<label>Idioma</label>
|
||||
<input type="text" id="tplLang" value="es_CO" placeholder="es_CO">
|
||||
</div>
|
||||
<div class="tpl-field">
|
||||
<label>Parámetros (JSON, opcional)</label>
|
||||
<input type="text" id="tplParams" placeholder='[{"type":"text","text":"valor"}]'>
|
||||
</div>
|
||||
<div class="tpl-actions">
|
||||
<button class="btn-tpl-cancel" onclick="closeTemplatePicker()">Cancelar</button>
|
||||
<button class="btn-tpl-send" onclick="sendTemplate()">Enviar</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const API_LIST = 'modules/turnero/api/chat_get_list.php';
|
||||
const API_MESSAGES = 'modules/turnero/api/chat_get_messages.php';
|
||||
const API_SEND = 'modules/turnero/api/chat_send_message.php';
|
||||
const API_READ = 'modules/turnero/api/chat_mark_read.php';
|
||||
const API_MEDIA = 'modules/turnero/api/chat_upload_media.php';
|
||||
const API_REACT = 'modules/turnero/api/chat_react.php';
|
||||
const BASE = (function() {
|
||||
const s = window.location.pathname;
|
||||
return s.replace(/\/erp\.php.*$/, '/') || '/';
|
||||
@@ -476,33 +617,56 @@ function appendMessage(msg) {
|
||||
}
|
||||
|
||||
function buildBubble(msg) {
|
||||
const div = document.createElement('div');
|
||||
const div = document.createElement('div');
|
||||
div.className = 'msg-bubble ' + (msg.direction === 'outgoing' ? 'outgoing' : 'incoming');
|
||||
div.dataset.msgId = msg.id;
|
||||
div.dataset.msgId = msg.id;
|
||||
div.dataset.waId = msg.message_id || '';
|
||||
|
||||
const mediaSrc = msg.local_file ? ('../' + msg.local_file) : (msg.media_url_external || null);
|
||||
let content = '';
|
||||
|
||||
if (msg.message_type === 'image' && (msg.local_file || msg.media_url_external)) {
|
||||
const src = msg.local_file ? ('../' + msg.local_file) : msg.media_url_external;
|
||||
content += `<img class="msg-media-thumb" src="${escHtml(src)}" onclick="window.open('${escHtml(src)}','_blank')" alt="imagen">`;
|
||||
if (msg.content) content += `<div>${escHtml(msg.content)}</div>`;
|
||||
} else if (['audio','video','document'].includes(msg.message_type) && (msg.local_file || msg.media_url_external)) {
|
||||
const src = msg.local_file ? ('../' + msg.local_file) : msg.media_url_external;
|
||||
const icon = msg.message_type === 'audio' ? 'fa-file-audio' : msg.message_type === 'video' ? 'fa-file-video' : 'fa-file-alt';
|
||||
content += `<a class="msg-media-link" href="${escHtml(src)}" target="_blank">
|
||||
<i class="fas ${icon}"></i> ${escHtml(msg.filename || msg.message_type)}
|
||||
if (msg.message_type === 'image' && mediaSrc) {
|
||||
content = `<img class="msg-img" src="${escHtml(mediaSrc)}" loading="lazy"
|
||||
onclick="window.open('${escHtml(mediaSrc)}','_blank')" alt="imagen">`;
|
||||
if (msg.content) content += `<div style="font-size:.82rem;margin-top:2px">${escHtml(msg.content)}</div>`;
|
||||
} else if (msg.message_type === 'video' && mediaSrc) {
|
||||
content = `<video class="msg-video" src="${escHtml(mediaSrc)}" controls preload="metadata"></video>`;
|
||||
if (msg.content) content += `<div style="font-size:.82rem;margin-top:2px">${escHtml(msg.content)}</div>`;
|
||||
} else if (msg.message_type === 'audio' && mediaSrc) {
|
||||
content = `<audio class="msg-audio" src="${escHtml(mediaSrc)}" controls preload="metadata"></audio>`;
|
||||
} else if (msg.message_type === 'document' && mediaSrc) {
|
||||
const icon = (msg.mime_type || '').includes('pdf') ? 'fa-file-pdf' :
|
||||
(msg.mime_type || '').includes('spreadsheet') || (msg.mime_type || '').includes('excel') ? 'fa-file-excel' :
|
||||
(msg.mime_type || '').includes('word') ? 'fa-file-word' : 'fa-file-alt';
|
||||
content = `<a class="msg-doc-link" href="${escHtml(mediaSrc)}" target="_blank">
|
||||
<i class="fas ${icon}" style="font-size:1.3rem"></i>
|
||||
<span>${escHtml(msg.filename || 'Documento')}</span>
|
||||
</a>`;
|
||||
} else {
|
||||
content = nl2br(escHtml(msg.content || ''));
|
||||
}
|
||||
|
||||
const time = msg.created_at ? formatTimeFull(msg.created_at) : '';
|
||||
div.innerHTML = content + `<div class="msg-time">${time}</div>`;
|
||||
const statusIcon = msg.direction === 'outgoing'
|
||||
? (msg.status === 'read' ? ' ✓✓' : msg.status === 'delivered' ? ' ✓✓' : ' ✓')
|
||||
: '';
|
||||
|
||||
const reactBtn = msg.message_id
|
||||
? `<button class="react-btn" onclick="showReactionPicker(event, '${escHtml(msg.message_id)}')">😊</button>`
|
||||
: '';
|
||||
|
||||
div.innerHTML = content
|
||||
+ `<div class="msg-time">${time}<span style="opacity:.7;font-size:.7rem">${statusIcon}</span></div>`
|
||||
+ (msg.reaction_emoji ? `<div class="msg-reaction">${escHtml(msg.reaction_emoji)}</div>` : '')
|
||||
+ reactBtn;
|
||||
return div;
|
||||
}
|
||||
|
||||
// ── Enviar ────────────────────────────────────────────────────────────────────
|
||||
// ── Enviar texto ─────────────────────────────────────────────────────────────
|
||||
async function sendMessage() {
|
||||
// Si hay archivo adjunto, enviar como media
|
||||
if (attachFile) { await sendMedia(); return; }
|
||||
|
||||
const input = document.getElementById('msgInput');
|
||||
const message = input.value.trim();
|
||||
if (!message || !state.activeUserId) return;
|
||||
@@ -534,6 +698,229 @@ async function sendMessage() {
|
||||
}
|
||||
}
|
||||
|
||||
// ── Adjuntar archivo ──────────────────────────────────────────────────────────
|
||||
let attachFile = null;
|
||||
|
||||
function onFileSelected(e) {
|
||||
const file = e.target.files[0];
|
||||
if (!file) return;
|
||||
attachFile = file;
|
||||
const prev = document.getElementById('attach-preview');
|
||||
const thumb = document.getElementById('attachThumb');
|
||||
const name = document.getElementById('attachName');
|
||||
name.textContent = file.name + ' (' + (file.size > 1048576 ? (file.size/1048576).toFixed(1) + ' MB' : Math.round(file.size/1024) + ' KB') + ')';
|
||||
if (file.type.startsWith('image/')) {
|
||||
thumb.src = URL.createObjectURL(file);
|
||||
thumb.style.display = 'block';
|
||||
} else {
|
||||
thumb.style.display = 'none';
|
||||
}
|
||||
prev.style.display = 'flex';
|
||||
e.target.value = '';
|
||||
}
|
||||
|
||||
function clearAttach() {
|
||||
attachFile = null;
|
||||
document.getElementById('attach-preview').style.display = 'none';
|
||||
document.getElementById('attachThumb').src = '';
|
||||
}
|
||||
|
||||
async function sendMedia() {
|
||||
if (!attachFile || !state.activeUserId) return;
|
||||
const btn = document.getElementById('btnSend');
|
||||
btn.disabled = true;
|
||||
const caption = document.getElementById('msgInput').value.trim();
|
||||
|
||||
const fd = new FormData();
|
||||
fd.append('user_id', state.activeUserId);
|
||||
fd.append('file', attachFile);
|
||||
if (caption) fd.append('caption', caption);
|
||||
|
||||
try {
|
||||
const res = await fetch(BASE + API_MEDIA, { method: 'POST', body: fd });
|
||||
const json = await res.json();
|
||||
if (!json.success) {
|
||||
alert('Error al enviar archivo: ' + (json.error || 'desconocido'));
|
||||
} else {
|
||||
clearAttach();
|
||||
document.getElementById('msgInput').value = '';
|
||||
autoResize(document.getElementById('msgInput'));
|
||||
await pollMessages();
|
||||
}
|
||||
} catch(e) {
|
||||
alert('Error de red al enviar el archivo');
|
||||
} finally {
|
||||
btn.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
// ── Emoji picker ──────────────────────────────────────────────────────────────
|
||||
const EMOJI_LIST = ['😀','😂','😍','😎','😢','😡','🤔','👍','👎','❤️','🔥','✅','⭐','🎉','🙏','💪','🤣','😅','😊','🥰','😘','🤗','😴','🤒','😷','💊','🏥','👨⚕️','👩⚕️','📋','📞','📅','⏰','💉','🩺'];
|
||||
|
||||
function toggleEmojiPanel() {
|
||||
const panel = document.getElementById('emojiPanel');
|
||||
if (!panel.classList.contains('show')) {
|
||||
panel.innerHTML = EMOJI_LIST.map(e => `<span onclick="insertEmoji('${e}')">${e}</span>`).join('');
|
||||
}
|
||||
panel.classList.toggle('show');
|
||||
}
|
||||
|
||||
function insertEmoji(em) {
|
||||
const inp = document.getElementById('msgInput');
|
||||
const pos = inp.selectionStart;
|
||||
inp.value = inp.value.slice(0, pos) + em + inp.value.slice(pos);
|
||||
inp.selectionStart = inp.selectionEnd = pos + em.length;
|
||||
inp.focus();
|
||||
autoResize(inp);
|
||||
document.getElementById('emojiPanel').classList.remove('show');
|
||||
}
|
||||
|
||||
document.addEventListener('click', e => {
|
||||
const panel = document.getElementById('emojiPanel');
|
||||
if (panel.classList.contains('show') && !e.target.closest('.chat-input-area')) {
|
||||
panel.classList.remove('show');
|
||||
}
|
||||
const rp = document.getElementById('reaction-picker');
|
||||
if (rp.classList.contains('show') && !e.target.closest('#reaction-picker') && !e.target.closest('.react-btn')) {
|
||||
rp.classList.remove('show');
|
||||
}
|
||||
});
|
||||
|
||||
// ── Reacciones ────────────────────────────────────────────────────────────────
|
||||
let _reactionTarget = null;
|
||||
|
||||
function showReactionPicker(e, waId) {
|
||||
e.stopPropagation();
|
||||
_reactionTarget = waId;
|
||||
const rp = document.getElementById('reaction-picker');
|
||||
rp.classList.add('show');
|
||||
const rect = e.target.getBoundingClientRect();
|
||||
rp.style.top = (rect.top - 60) + 'px';
|
||||
rp.style.left = Math.max(4, rect.left - 80) + 'px';
|
||||
}
|
||||
|
||||
async function sendReactionEmoji(emoji) {
|
||||
document.getElementById('reaction-picker').classList.remove('show');
|
||||
if (!_reactionTarget || !state.activeUserId) return;
|
||||
try {
|
||||
await fetch(BASE + API_REACT, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ user_id: state.activeUserId, message_id: _reactionTarget, emoji })
|
||||
});
|
||||
await pollMessages();
|
||||
} catch(e) { console.error('reaction:', e); }
|
||||
}
|
||||
|
||||
// ── Grabación de audio ────────────────────────────────────────────────────────
|
||||
let mediaRecorder = null, audioChunks = [], recInterval = null, recSeconds = 0;
|
||||
|
||||
async function toggleRecording() {
|
||||
if (mediaRecorder && mediaRecorder.state === 'recording') {
|
||||
stopRecording();
|
||||
} else {
|
||||
await startRecording();
|
||||
}
|
||||
}
|
||||
|
||||
async function startRecording() {
|
||||
try {
|
||||
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
||||
audioChunks = [];
|
||||
mediaRecorder = new MediaRecorder(stream, { mimeType: MediaRecorder.isTypeSupported('audio/webm') ? 'audio/webm' : 'audio/ogg' });
|
||||
mediaRecorder.ondataavailable = e => { if (e.data.size > 0) audioChunks.push(e.data); };
|
||||
mediaRecorder.start(100);
|
||||
|
||||
recSeconds = 0;
|
||||
document.getElementById('recording-bar').style.display = 'flex';
|
||||
document.getElementById('recTimer').textContent = '0:00';
|
||||
document.getElementById('micBtn').style.color = '#e53e3e';
|
||||
recInterval = setInterval(() => {
|
||||
recSeconds++;
|
||||
const m = Math.floor(recSeconds/60), s = recSeconds%60;
|
||||
document.getElementById('recTimer').textContent = m + ':' + String(s).padStart(2,'0');
|
||||
}, 1000);
|
||||
} catch(e) {
|
||||
alert('No se pudo acceder al micrófono: ' + e.message);
|
||||
}
|
||||
}
|
||||
|
||||
async function stopRecording() {
|
||||
if (!mediaRecorder) return;
|
||||
clearInterval(recInterval);
|
||||
document.getElementById('recording-bar').style.display = 'none';
|
||||
document.getElementById('micBtn').style.color = '';
|
||||
|
||||
return new Promise(resolve => {
|
||||
mediaRecorder.onstop = async () => {
|
||||
const mime = mediaRecorder.mimeType || 'audio/webm';
|
||||
const blob = new Blob(audioChunks, { type: mime });
|
||||
const ext = mime.includes('ogg') ? 'ogg' : 'webm';
|
||||
const file = new File([blob], `audio_${Date.now()}.${ext}`, { type: mime });
|
||||
mediaRecorder.stream.getTracks().forEach(t => t.stop());
|
||||
mediaRecorder = null;
|
||||
|
||||
if (!state.activeUserId) { resolve(); return; }
|
||||
const fd = new FormData();
|
||||
fd.append('user_id', state.activeUserId);
|
||||
fd.append('file', file);
|
||||
fd.append('is_voice', '1');
|
||||
try {
|
||||
const res = await fetch(BASE + API_MEDIA, { method: 'POST', body: fd });
|
||||
const json = await res.json();
|
||||
if (json.success) await pollMessages();
|
||||
else alert('Error al enviar audio: ' + (json.error || ''));
|
||||
} catch(e) { alert('Error de red al enviar audio'); }
|
||||
resolve();
|
||||
};
|
||||
mediaRecorder.stop();
|
||||
});
|
||||
}
|
||||
|
||||
function cancelRecording() {
|
||||
if (!mediaRecorder) return;
|
||||
clearInterval(recInterval);
|
||||
mediaRecorder.stream.getTracks().forEach(t => t.stop());
|
||||
mediaRecorder = null;
|
||||
audioChunks = [];
|
||||
document.getElementById('recording-bar').style.display = 'none';
|
||||
document.getElementById('micBtn').style.color = '';
|
||||
}
|
||||
|
||||
// ── Plantilla ─────────────────────────────────────────────────────────────────
|
||||
function openTemplatePicker() {
|
||||
if (!state.activeUserId) return;
|
||||
document.getElementById('template-modal').classList.add('show');
|
||||
document.getElementById('tplName').focus();
|
||||
}
|
||||
function closeTemplatePicker() {
|
||||
document.getElementById('template-modal').classList.remove('show');
|
||||
}
|
||||
|
||||
async function sendTemplate() {
|
||||
const name = document.getElementById('tplName').value.trim();
|
||||
const lang = document.getElementById('tplLang').value.trim() || 'es_CO';
|
||||
const rawP = document.getElementById('tplParams').value.trim();
|
||||
if (!name || !state.activeUserId) return alert('Ingresa el nombre de la plantilla');
|
||||
|
||||
let params = [];
|
||||
if (rawP) {
|
||||
try { params = JSON.parse(rawP); } catch(e) { return alert('Los parámetros no son JSON válido'); }
|
||||
}
|
||||
|
||||
closeTemplatePicker();
|
||||
try {
|
||||
const res = await fetch(BASE + API_SEND, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ user_id: state.activeUserId, type: 'template', template: name, lang, params })
|
||||
});
|
||||
const json = await res.json();
|
||||
if (!json.success) alert('Error al enviar plantilla: ' + (json.error || ''));
|
||||
else await pollMessages();
|
||||
} catch(e) { alert('Error de red al enviar plantilla'); }
|
||||
}
|
||||
|
||||
async function markRead(userId) {
|
||||
try {
|
||||
await fetch(BASE + API_READ, {
|
||||
|
||||
Reference in New Issue
Block a user