Presiden Republik Indonesia Prabowo Subianto

Presiden Republik Indonesia Prabowo Subianto melakukan pertemuan bilateral dengan Presiden Federasi Rusia Vladimir Putin di Vladivostok, Rusia, Kamis (3/9/2026). Pertemuan tersebut berlangsung di sela-sela penyelenggaraan Forum Ekonomi Timur (Eastern Economic Forum/EEF) ke-11 dalam format working lunch.

Dalam sambutannya, Presiden Vladimir Putin menyambut hangat kedatangan Presiden Prabowo di Vladivostok. Putin menilai intensitas pertemuan dan komunikasi langsung kedua pemimpin mencerminkan sifat strategis hubungan bilateral Rusia dan Indonesia.

“Yang Mulia Bapak Presiden, izinkan saya mengucapkan selamat datang ke Kota Vladivostok. Pertemuan hari ini merupakan pertemuan kedua pada tahun ini. Dan kontak kita yang pribadi mencerminkan sifat strategis dari hubungan bilateral antara Rusia dan Indonesia,” ujar Putin.

Putin juga menyampaikan apresiasi atas kesediaan Prabowo berkunjung ke Vladivostok sekaligus menghadiri EEF ke-11 sebagai tamu kehormatan utama.

“Kami sangat senang dan gembira menerima kunjungan dari Bapak hari ini. Selamat datang,” ungkap Putin.

Merespons sambutan tersebut, Presiden Prabowo menyampaikan terima kasih atas sambutan hangat dan keramahan pemerintah Rusia selama kunjungannya.

“Terima kasih, Yang Mulia, Presiden Putin. Pertama-tama, saya senang sekali berjumpa lagi dengan Yang Mulia. Terima kasih atas sambutan yang hangat dan keramahan yang diberikan kepada saya. Saya merasa sangat terhormat dan gembira dapat mengunjungi Vladivostok,” ujar Presiden Prabowo.

Dalam pertemuan itu, Prabowo juga menyampaikan ucapan selamat atas peringatan Hari Nasional Federasi Rusia pada 12 Juni 2026 serta penyelenggaraan KTT Peringatan ASEAN-Rusia ke-35 pada 18 Juni 2026.

Prabowo sekaligus menyampaikan permohonan maaf karena tidak dapat menghadiri kedua momentum tersebut secara langsung akibat adanya persoalan dalam negeri yang tidak dapat ditinggalkan.

“Saya mohon maaf sebesar-besarnya karena saya tidak dapat menghadiri acara penting tersebut karena ada masalah dalam negeri yang tidak bisa saya tinggalkan,” katanya.

Meski demikian, Prabowo menegaskan kunjungannya ke Vladivostok merupakan bentuk penghormatan sekaligus upaya memenuhi komitmen dalam memperkuat hubungan Indonesia dan Rusia.

“Tapi saya membayar hutang saya dengan hadir di Vladivostok sekarang. Sekali lagi terima kasih,” ucap Presiden.

Dalam pertemuan tersebut, Presiden Prabowo didampingi Menteri Luar Negeri Sugiono, Menteri Koordinator Bidang Perekonomian Airlangga Hartarto, Menteri Investasi dan Hilirisasi/Kepala BKPM Rosan Roeslani, Menteri Energi dan Sumber Daya Mineral Bahlil Lahadalia, Menteri Pendidikan Tinggi, Sains, dan Teknologi Brian Yuliarto, Sekretaris Kabinet Teddy Indra Wijaya, serta Duta Besar Republik Indonesia untuk Federasi Rusia merangkap Republik Belarus Jose Antonio Morato Tavares.

Facebook
Twitter
WhatsApp
X
Telegram
Facebook
Twitter
WhatsApp
`, 'text/html'); const allowedTags = new Set([ 'a','abbr','b','blockquote','br','code','del','div','em','figure','figcaption','h1','h2','h3','h4','h5','h6','hr','i','img','li','ol','p','pre','s','span','strong','sub','sup','table','tbody','td','th','thead','tr','u','ul' ]); const urlAttrs = { a: ['href'], img: ['src'], }; const extraAttrs = { a: ['title','target','rel'], code: ['class'], pre: ['class'], div: ['class'], span: ['class'], table: ['class'], th: ['class','scope'], td: ['class'], img: ['alt','title','width','height','class'], }; const globalAttrs = ['class']; function cleanse(node){ const children = Array.from(node.childNodes); children.forEach((child) => { if (child.nodeType === Node.ELEMENT_NODE) { const tag = child.tagName.toLowerCase(); if (!allowedTags.has(tag)) { if (child.childNodes.length) { while (child.firstChild) { node.insertBefore(child.firstChild, child); } } node.removeChild(child); return; } const allowed = new Set([...(extraAttrs[tag] || []), ...globalAttrs]); Array.from(child.attributes).forEach((attr) => { const name = attr.name.toLowerCase(); if (!allowed.has(name)) { child.removeAttribute(attr.name); return; } if (name === 'class') { const safeClass = kcSanitizeClassList(attr.value); if (safeClass) { child.setAttribute('class', safeClass); } else { child.removeAttribute('class'); } return; } const shouldSanitizeUrl = (urlAttrs[tag] || []).includes(name); if (shouldSanitizeUrl) { const safeUrl = kcSanitizeLinkUrl(attr.value); if (!safeUrl) { child.removeAttribute(attr.name); } else { child.setAttribute(attr.name, safeUrl); if (tag === 'a') { child.setAttribute('rel', 'noopener noreferrer'); child.setAttribute('target', '_blank'); } } } }); cleanse(child); } else if (child.nodeType === Node.COMMENT_NODE) { node.removeChild(child); } else if (child.nodeType !== Node.TEXT_NODE) { node.removeChild(child); } }); } cleanse(doc.body); return doc.body.innerHTML; } function kcBuildParsedNode(raw, format){ if (typeof raw !== 'string') { return null; } const trimmed = raw.trim(); if (!trimmed) { return null; } if (format === 'html') { const sanitized = kcSanitizeHtmlFragment(raw); if (!sanitized) { return null; } const container = document.createElement('div'); container.className = 'kc-rich-content'; container.innerHTML = sanitized; return container; } if (format === 'latex') { const container = document.createElement('div'); container.className = 'kc-rich-content'; const pre = document.createElement('pre'); pre.className = 'kc-format-latex'; pre.textContent = raw; container.appendChild(pre); return container; } if (format === 'bbcode') { const html = kcBuildBbcodeHtml(raw); if (!html) { return null; } const container = document.createElement('div'); container.className = 'kc-rich-content'; container.innerHTML = html; return container; } const html = kcBuildMarkdownishHtml(raw); if (!html) { return null; } const container = document.createElement('div'); container.className = 'kc-rich-content'; container.innerHTML = html; return container; } function init(){ const root = document.getElementById(ROOT_ID); if (!root) { return; } const TITLE = root?.dataset?.title || 'AI Chat'; const BUTTON_LABEL = root?.dataset?.buttonlabel || TITLE; let proxyId = root?.dataset?.proxyid || ''; const CONFIG_KEY = root?.dataset?.configkey || ''; const REST_NONCE = root?.dataset?.restnonce || ''; const WP_UID = root?.dataset?.wpuserid ? String(root.dataset.wpuserid) : '0'; const WP_NAME = root?.dataset?.wpdisplay || ''; const AVATAR_URL = root?.dataset?.avatar || ''; const parsingEnabledAttr = (root?.dataset?.parsingenabled || '').toLowerCase(); const PARSING_ENABLED = ['1','true','yes','on'].includes(parsingEnabledAttr); const parsingFormatAttr = (root?.dataset?.parsingformat || '').toLowerCase(); const PARSING_FORMAT = kcNormalizeParsingFormat(parsingFormatAttr); const typingModeAttr = (root?.dataset?.typingmode || 'off').toLowerCase(); const TYPING_MODE = typingModeAttr === 'wpm' ? 'wpm' : 'off'; const typingWpmAttr = parseInt(root?.dataset?.typingwpm ? root.dataset.typingwpm : '', 10); const TYPING_WPM = normalizeTypingWpm(typingWpmAttr); const maxCharsAttr = parseInt(root?.dataset?.maxchars ? root.dataset.maxchars : '', 10); const MAX_USER_CHARS = normalizeMaxInputChars(maxCharsAttr); const frameModeAttr = (root?.dataset?.framemode || 'fixed').toLowerCase(); const FRAME_MODE = frameModeAttr === 'dynamic' ? 'dynamic' : 'fixed'; const frameWidthAttr = parseInt(root?.dataset?.framewidth ? root.dataset.framewidth : '', 10); const frameHeightAttr = parseInt(root?.dataset?.frameheight ? root.dataset.frameheight : '', 10); let frameWidth = Number.isFinite(frameWidthAttr) && frameWidthAttr > 0 ? frameWidthAttr : 340; let frameHeight = Number.isFinite(frameHeightAttr) && frameHeightAttr > 0 ? frameHeightAttr : 460; const panelSideAttr = (root?.dataset?.side || 'right').toLowerCase(); const PANEL_SIDE = panelSideAttr === 'left' ? 'left' : 'right'; const welcomeEnabledAttr = (root?.dataset?.welcomeenabled || '').toLowerCase(); const WELCOME_ENABLED = ['1','true','yes','on'].includes(welcomeEnabledAttr); const WELCOME_MESSAGE = (root?.dataset?.welcomemessage || '').replace(/\r\n/g, '\n'); const WELCOME_MESSAGE_TEXT = WELCOME_MESSAGE.trim(); const COPY_BUTTON_ICON = ''; const FRAME_STORAGE_KEY = FRAME_MODE === 'dynamic' ? `${ROOT_ID}_frame_size` : ''; const REST_URL = root?.dataset?.resturl || ((window.wpApiSettings && window.wpApiSettings.root) ? window.wpApiSettings.root.replace(/\/$/,'') + '/masgar-chatbot/v1/talk' : (window.location.origin + '/wp-json/masgar-chatbot/v1/talk')); const panel = document.getElementById(PANEL_ID); const openBtn = document.getElementById(BUTTON_ID); if (openBtn) { openBtn.setAttribute('aria-label', BUTTON_LABEL); const openLabel = openBtn.querySelector('.kc-label'); if (openLabel) openLabel.textContent = BUTTON_LABEL; } if (!panel || !openBtn) return; const log = document.getElementById(LOG_ID); const input = document.getElementById(INPUT_ID); const send = document.getElementById(SEND_ID); const statusEl = document.getElementById(STATUS_ID); const titleEl = document.getElementById(TITLE_ID); if(titleEl) titleEl.textContent = TITLE; if (input) { const safeMaxChars = Number.isFinite(MAX_USER_CHARS) ? MAX_USER_CHARS : 1000; if (safeMaxChars > 0) { input.setAttribute('maxlength', String(safeMaxChars)); } else { input.removeAttribute('maxlength'); } } const avatarViewer = document.getElementById(AVATAR_VIEWER_ID); const avatarPreviewImg = document.getElementById(AVATAR_PREVIEW_ID); const avatarCloseBtn = document.getElementById(AVATAR_CLOSE_ID); const PREVIEWABLE_SELECTOR = '.kc-avatar:not(.kc-btn-avatar)'; const SCROLL_STICKY_THRESHOLD = 24; let scrollButton = null; function normalizeMaxInputChars(value){ let numeric = Number.isFinite(value) ? value : Number(value); if (!Number.isFinite(numeric) || numeric <= 0) { numeric = 1000; } numeric = Math.round(numeric); if (!Number.isFinite(numeric) || numeric <= 0) { numeric = 1000; } if (numeric < 20) { numeric = 20; } if (numeric > 4000) { numeric = 4000; } return numeric; } function normalizeTypingWpm(value){ let numeric = Number.isFinite(value) ? value : 600; if (numeric <= 0) { numeric = 600; } let rounded = Math.round(numeric / 10) * 10; if (!Number.isFinite(rounded)) { rounded = 600; } if (rounded < 100) { rounded = 100; } if (rounded > 5000) { rounded = 5000; } return rounded; } if (log) { scrollButton = document.createElement('button'); scrollButton.type = 'button'; scrollButton.className = 'kc-scroll-bottom'; scrollButton.setAttribute('aria-label', 'Scroll ke pesan terakhir'); scrollButton.innerHTML = 'Pesan terbaru'; scrollButton.setAttribute('tabindex', '-1'); log.appendChild(scrollButton); window.requestAnimationFrame(updateScrollButtonPosition); } function appendToLog(element){ if (!log || !element) return; if (scrollButton && log.contains(scrollButton)) { log.insertBefore(element, scrollButton); } else { log.appendChild(element); } window.requestAnimationFrame(updateScrollButtonPosition); } function isLogNearBottom(){ if (!log) return true; const gap = log.scrollHeight - log.scrollTop - log.clientHeight; return gap <= SCROLL_STICKY_THRESHOLD; } function updateScrollButtonPosition(){ if (!log || !scrollButton) return; const scrollTop = log.scrollTop; const viewportHeight = log.clientHeight; const buttonRect = scrollButton.getBoundingClientRect(); const buttonHeight = Number.isFinite(buttonRect.height) && buttonRect.height > 0 ? buttonRect.height : (scrollButton.offsetHeight || 0); const bottomOffset = 18; const topOffset = 12; let desiredTop = scrollTop + viewportHeight - buttonHeight - bottomOffset; if (!Number.isFinite(desiredTop)) { desiredTop = scrollTop + viewportHeight - bottomOffset; } const minTop = scrollTop + topOffset; if (!Number.isFinite(desiredTop) || desiredTop < minTop) { desiredTop = minTop; } scrollButton.style.top = `${desiredTop}px`; } function updateScrollState(){ if (!log) return; const nearBottom = isLogNearBottom(); log.classList.toggle('kc-scroll-pending', !nearBottom); if (scrollButton) { if (nearBottom) { scrollButton.setAttribute('tabindex', '-1'); } else { scrollButton.removeAttribute('tabindex'); } } updateScrollButtonPosition(); } function scrollToBottom(options){ if (!log) return; const opts = (options && typeof options === 'object') ? options : {}; const behavior = opts.behavior === 'smooth' ? 'smooth' : 'auto'; if (typeof log.scrollTo === 'function') { log.scrollTo({ top: log.scrollHeight, behavior }); } else { log.scrollTop = log.scrollHeight; } window.requestAnimationFrame(updateScrollState); } if (scrollButton) { scrollButton.addEventListener('click', () => { scrollToBottom({ behavior: 'smooth' }); }); } if (log) { log.addEventListener('scroll', () => { updateScrollState(); }); updateScrollState(); } window.addEventListener('resize', () => { window.requestAnimationFrame(updateScrollButtonPosition); }); let isSending = false; let typingAnimationHandle = null; let copyFeedbackTimer = null; let ignoreNextAvatarDocClick = false; const FALLBACK_RETRY_MESSAGE = 'Maaf, saya belum menerima balasan dari server. Silakan kirim ulang atau muat ulang halaman.'; const GENERIC_SERVER_ERROR_MESSAGE = 'Gagal menghubungi server. Silakan kirim ulang atau muat ulang halaman.'; const SCRATCH_SANITIZER = document.createElement('div'); function toPlainText(value){ if (typeof value !== 'string') return ''; SCRATCH_SANITIZER.innerHTML = value; const text = SCRATCH_SANITIZER.textContent || ''; SCRATCH_SANITIZER.textContent = ''; return text.trim(); } async function writeTextToClipboard(text){ if (typeof text !== 'string') { if (text === undefined || text === null) return false; text = String(text); } if (!text) return false; if (navigator.clipboard && typeof navigator.clipboard.writeText === 'function') { try { await navigator.clipboard.writeText(text); return true; } catch(_) { /* fallback below */ } } const textarea = document.createElement('textarea'); textarea.value = text; textarea.setAttribute('readonly', ''); textarea.style.position = 'fixed'; textarea.style.opacity = '0'; textarea.style.pointerEvents = 'none'; textarea.style.zIndex = '-1'; document.body.appendChild(textarea); const activeEl = document.activeElement; try { textarea.focus({ preventScroll: true }); } catch(_) { textarea.focus(); } textarea.select(); let success = false; try { success = document.execCommand('copy'); } catch(_) { success = false; } textarea.blur(); document.body.removeChild(textarea); if (activeEl && typeof activeEl.focus === 'function') { try { activeEl.focus({ preventScroll: true }); } catch(_) { activeEl.focus(); } } return success; } function showCopyFeedback(success){ if (!statusEl || isSending) return; const message = success ? 'Pesan tersalin ke clipboard.' : 'Gagal menyalin pesan.'; statusEl.textContent = message; if (copyFeedbackTimer) { window.clearTimeout(copyFeedbackTimer); } copyFeedbackTimer = window.setTimeout(() => { if (!isSending && statusEl.textContent === message) { statusEl.textContent = ''; } }, success ? 2400 : 3200); } function attachCopyControl(bubble, textForCopy){ if (!bubble) return; const copyText = typeof textForCopy === 'string' ? textForCopy : ''; const trimmed = copyText.trim(); bubble.classList.remove('kc-has-copy'); const existing = bubble.querySelectorAll('.kc-copy-btn'); if (existing.length) { existing.forEach((btn) => { if (btn && btn.parentNode === bubble) { btn.parentNode.removeChild(btn); } }); } if (!trimmed) { return; } const copyBtn = document.createElement('button'); copyBtn.type = 'button'; copyBtn.className = 'kc-copy-btn'; copyBtn.setAttribute('aria-label', 'Salin pesan'); copyBtn.setAttribute('title', 'Salin pesan'); copyBtn.innerHTML = COPY_BUTTON_ICON; copyBtn.addEventListener('click', (ev) => { ev.preventDefault(); ev.stopPropagation(); const valueToCopy = copyText || trimmed; writeTextToClipboard(valueToCopy).then((ok) => { showCopyFeedback(!!ok); }).catch(() => { showCopyFeedback(false); }); }); bubble.classList.add('kc-has-copy'); bubble.appendChild(copyBtn); } function isAvatarViewerOpen(){ return !!(avatarViewer && avatarViewer.classList.contains('kc-show')); } function closeAvatarPreview(){ if(!avatarViewer || !avatarPreviewImg) return; avatarViewer.classList.remove('kc-show'); avatarViewer.setAttribute('aria-hidden','true'); avatarPreviewImg.removeAttribute('src'); avatarPreviewImg.alt = ''; ignoreNextAvatarDocClick = false; } function openAvatarPreview(src, altText){ if(!avatarViewer || !avatarPreviewImg || !src) return; ignoreNextAvatarDocClick = true; avatarPreviewImg.src = src; avatarPreviewImg.alt = altText || 'Avatar'; avatarViewer.classList.add('kc-show'); avatarViewer.setAttribute('aria-hidden','false'); setTimeout(()=>{ ignoreNextAvatarDocClick = false; }, 0); } function bindAvatarPreview(scope){ if(!avatarViewer) return; const context = (scope && scope.querySelectorAll) ? scope : document; const avatars = context.querySelectorAll(PREVIEWABLE_SELECTOR); avatars.forEach((img)=>{ if(!(img instanceof HTMLImageElement)) return; if(img.dataset.kcPreviewBound === '1') return; img.dataset.kcPreviewBound = '1'; img.classList.add('kc-avatar-zoomable'); img.addEventListener('click', (ev)=>{ ev.preventDefault(); ev.stopPropagation(); const src = img.currentSrc || img.getAttribute('src'); if(!src) return; const alt = img.getAttribute('alt') || 'Avatar'; openAvatarPreview(src, alt); }); }); } if(avatarViewer){ avatarViewer.addEventListener('click', (ev)=>{ if(ev.target === avatarViewer){ closeAvatarPreview(); } }); } if(avatarCloseBtn){ avatarCloseBtn.addEventListener('click', closeAvatarPreview); } document.addEventListener('click', ()=>{ if(!isAvatarViewerOpen()) return; if(ignoreNextAvatarDocClick){ ignoreNextAvatarDocClick = false; return; } closeAvatarPreview(); }); function cancelTypingAnimation(){ if (typingAnimationHandle && typeof typingAnimationHandle.cancel === 'function') { try { typingAnimationHandle.cancel(); } catch(_) { /* noop */ } } typingAnimationHandle = null; } function clampFrameWidth(value){ const min = 260; const viewportLimit = (typeof window !== 'undefined' && typeof window.innerWidth === 'number') ? Math.max(min, window.innerWidth - 40) : min; const maxAllowed = Math.max(min, Math.min(viewportLimit, 800)); const numeric = Number.isFinite(value) ? value : min; return Math.min(Math.max(numeric, min), maxAllowed); } function clampFrameHeight(value){ const min = 320; const viewportLimit = (typeof window !== 'undefined' && typeof window.innerHeight === 'number') ? Math.max(min, window.innerHeight - 160) : min; const maxAllowed = Math.max(min, Math.min(viewportLimit, 900)); const numeric = Number.isFinite(value) ? value : min; return Math.min(Math.max(numeric, min), maxAllowed); } function readStoredFrameSize(){ if (FRAME_MODE !== 'dynamic' || !FRAME_STORAGE_KEY) { return null; } try { const raw = localStorage.getItem(FRAME_STORAGE_KEY); if (!raw) return null; const parsed = JSON.parse(raw); if (!parsed || typeof parsed !== 'object') return null; const storedWidth = Number(parsed.width); const storedHeight = Number(parsed.height); if (!Number.isFinite(storedWidth) || !Number.isFinite(storedHeight)) { return null; } return { width: clampFrameWidth(storedWidth), height: clampFrameHeight(storedHeight), }; } catch(_) { return null; } } function applyFrameDimensions(width, height, options){ if (!panel) return; const opts = (options && typeof options === 'object') ? options : {}; const shouldPersist = opts.persist === true; const desiredWidth = Number.isFinite(width) ? width : frameWidth; const desiredHeight = Number.isFinite(height) ? height : frameHeight; frameWidth = clampFrameWidth(Math.round(desiredWidth)); frameHeight = clampFrameHeight(Math.round(desiredHeight)); panel.style.setProperty('--kc-panel-width', `${frameWidth}px`); panel.style.setProperty('--kc-panel-height', `${frameHeight}px`); panel.style.width = `${frameWidth}px`; panel.style.height = `${frameHeight}px`; if (FRAME_MODE === 'dynamic' && shouldPersist && FRAME_STORAGE_KEY) { try { localStorage.setItem(FRAME_STORAGE_KEY, JSON.stringify({ width: frameWidth, height: frameHeight })); } catch(_) { /* noop */ } } window.requestAnimationFrame(updateScrollButtonPosition); } function setupResizeHandle(){ if (!panel || FRAME_MODE !== 'dynamic') return; const handle = panel.querySelector('.kc-resize-handle'); if (!handle) return; if (handle.dataset.kcResizeBound === '1') return; handle.dataset.kcResizeBound = '1'; handle.addEventListener('pointerdown', (event) => { if (typeof event.button === 'number' && event.button !== 0) { return; } event.preventDefault(); handle.classList.add('kc-active'); const startX = event.clientX; const startY = event.clientY; const rect = panel.getBoundingClientRect(); const startWidth = rect.width; const startHeight = rect.height; const pointerId = event.pointerId; const horizontalFactor = PANEL_SIDE === 'right' ? -1 : 1; const shouldStickToBottom = isLogNearBottom(); if (handle.setPointerCapture && pointerId !== undefined) { try { handle.setPointerCapture(pointerId); } catch(_) { /* noop */ } } const onMove = (ev) => { if (pointerId !== undefined && ev.pointerId !== undefined && ev.pointerId !== pointerId) { return; } const deltaX = (ev.clientX - startX) * horizontalFactor; const newWidth = startWidth + deltaX; const newHeight = startHeight + (ev.clientY - startY); applyFrameDimensions(newWidth, newHeight, { persist: false }); if (log) { if (shouldStickToBottom) { scrollToBottom({ behavior: 'auto' }); } else { updateScrollState(); } } }; const onEnd = (ev) => { if (pointerId !== undefined && ev.pointerId !== undefined && ev.pointerId !== pointerId) { return; } window.removeEventListener('pointermove', onMove); window.removeEventListener('pointerup', onEnd); window.removeEventListener('pointercancel', onEnd); if (handle.releasePointerCapture && pointerId !== undefined) { try { handle.releasePointerCapture(pointerId); } catch(_) { /* noop */ } } handle.classList.remove('kc-active'); applyFrameDimensions(frameWidth, frameHeight, { persist: true }); if (log) { if (shouldStickToBottom) { scrollToBottom({ behavior: 'auto' }); } else { updateScrollState(); } } }; window.addEventListener('pointermove', onMove); window.addEventListener('pointerup', onEnd); window.addEventListener('pointercancel', onEnd); }); } if (FRAME_MODE === 'dynamic') { const storedSize = readStoredFrameSize(); if (storedSize) { frameWidth = storedSize.width; frameHeight = storedSize.height; } } applyFrameDimensions(frameWidth, frameHeight, { persist: false }); panel.classList.toggle('kc-resizable', FRAME_MODE === 'dynamic'); if (FRAME_MODE === 'dynamic') { setupResizeHandle(); } window.addEventListener('resize', () => { applyFrameDimensions(frameWidth, frameHeight, { persist: FRAME_MODE === 'dynamic' }); }); const historyKeyRaw = (root?.dataset?.historykey || 'default').toString(); const HISTORY_KEY_SUFFIX = historyKeyRaw.replace(/[^a-z0-9_-]/gi, '').toLowerCase() || 'default'; const sessionStore = window.MasgarChatSession.create({ localStorage: () => window.localStorage, sessionStorage: () => window.sessionStorage, crypto: window.crypto, scope: `${HISTORY_KEY_SUFFIX}_${WP_UID}`, }); let sessionState = sessionStore.current(); const VISITOR_ID = sessionStore.visitorId; let SESSION_ID = sessionState.id; let SESSION_HEX = SESSION_ID.replace(/-/g, '').toLowerCase(); let activeRequestController = null; const timeoutSeconds = Number(root?.dataset?.requesttimeout) || 30; const REQUEST_TIMEOUT_MS = (Math.min(600, Math.max(5, timeoutSeconds)) + 5) * 1000; const historyModeAttr = (root?.dataset?.historymode || 'ttl').toLowerCase(); const HISTORY_MODE = historyModeAttr === 'count' ? 'count' : 'ttl'; const ttlAttr = parseInt(root?.dataset?.historyttl ?? '', 10); const HISTORY_TTL_MINUTES = Number.isFinite(ttlAttr) && ttlAttr > 0 ? ttlAttr : 360; const HISTORY_TTL_MS = HISTORY_TTL_MINUTES * 60 * 1000; const maxAttr = parseInt(root?.dataset?.historymax ?? '', 10); const HISTORY_MAX_ITEMS = Number.isFinite(maxAttr) && maxAttr > 0 ? maxAttr : 50; let HISTORY_STORAGE_KEY = SESSION_HEX ? `masgar_chat_history_${VISITOR_ID}_${SESSION_HEX}_${HISTORY_KEY_SUFFIX}` : `masgar_chat_history_${VISITOR_ID}_${HISTORY_KEY_SUFFIX}`; const LEGACY_VISITOR_HISTORY_KEY = `masgar_chat_history_${VISITOR_ID}_${HISTORY_KEY_SUFFIX}`; let LEGACY_HISTORY_KEY = SESSION_ID ? `masgar_chat_history_${SESSION_ID}` : null; let history = []; let welcomeSent = false; function clearHistoryStorage(){ const keys = new Set([HISTORY_STORAGE_KEY]); if (LEGACY_VISITOR_HISTORY_KEY && LEGACY_VISITOR_HISTORY_KEY !== HISTORY_STORAGE_KEY) { keys.add(LEGACY_VISITOR_HISTORY_KEY); } if (LEGACY_HISTORY_KEY && LEGACY_HISTORY_KEY !== HISTORY_STORAGE_KEY && LEGACY_HISTORY_KEY !== LEGACY_VISITOR_HISTORY_KEY) { keys.add(LEGACY_HISTORY_KEY); } keys.forEach((storageKey) => { if (!storageKey) return; try { localStorage.removeItem(storageKey); } catch(_){ /* noop */ } }); welcomeSent = false; } function readHistoryFromStorage(storageKey){ const fallback = { items: [], sessionId: null }; if(!storageKey) return fallback; let raw = null; try { raw = localStorage.getItem(storageKey); } catch(_){ raw = null; } if(!raw) return fallback; try { const parsed = JSON.parse(raw); if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) { const storedSessionId = typeof parsed.sessionId === 'string' ? parsed.sessionId : null; return { items: Array.isArray(parsed.items) ? parsed.items : [], sessionId: storedSessionId, }; } if (Array.isArray(parsed)) { return { items: parsed, sessionId: null }; } return fallback; } catch(_){ return fallback; } } function sanitizeHistoryItems(items){ const now = Date.now(); if (!Array.isArray(items)) return []; return items .filter((entry) => entry && typeof entry.text === 'string' && entry.text !== '') .map((entry) => { const rawTs = Number(entry.ts); return { role: entry.role === 'bot' ? 'bot' : 'user', text: entry.text, ts: Number.isFinite(rawTs) ? rawTs : now, }; }) .filter((entry) => { if (entry.ts < sessionState.createdAt || entry.ts > now || now >= sessionState.expiresAt) return false; if (HISTORY_MODE === 'ttl') { return (now - entry.ts) <= HISTORY_TTL_MS; } return true; }); } function applyHistoryPolicy(items){ let sanitized = sanitizeHistoryItems(items); sanitized.sort((a, b) => { if (a.ts === b.ts) return 0; return a.ts < b.ts ? -1 : 1; }); const seen = new Set(); sanitized = sanitized.filter((entry) => { const key = `${entry.role}|${entry.ts}|${entry.text}`; if (seen.has(key)) return false; seen.add(key); return true; }); if (HISTORY_MODE === 'count' && sanitized.length > HISTORY_MAX_ITEMS) { sanitized = sanitized.slice(-HISTORY_MAX_ITEMS); } return sanitized; } function persistHistory(){ history = applyHistoryPolicy(history); if (!history.length){ clearHistoryStorage(); return; } const payload = { version: 2, savedAt: Date.now(), expiresAt: sessionState.expiresAt, mode: HISTORY_MODE, ttlMinutes: HISTORY_TTL_MINUTES, maxItems: HISTORY_MAX_ITEMS, sessionId: SESSION_HEX || null, visitorId: VISITOR_ID, items: history, }; try { localStorage.setItem(HISTORY_STORAGE_KEY, JSON.stringify(payload)); } catch(_){ /* noop */ } if (LEGACY_VISITOR_HISTORY_KEY && LEGACY_VISITOR_HISTORY_KEY !== HISTORY_STORAGE_KEY) { try { localStorage.removeItem(LEGACY_VISITOR_HISTORY_KEY); } catch(_){ /* noop */ } } } function addHistoryEntry(role, text){ if (!text) return; history.push({ role: role === 'bot' ? 'bot' : 'user', text, ts: Date.now() }); persistHistory(); } let isOpen = false; function openPanel(){ ensureCurrentSession(); if(isOpen) return; isOpen=true; panel.classList.remove('kc-close'); panel.classList.add('kc-open'); panel.style.display='flex'; panel.setAttribute('aria-hidden','false'); openBtn.setAttribute('aria-expanded','true'); maybeSendWelcome(); setTimeout(()=> input?.focus(), 600); } function closePanel(){ if(!isOpen) return; isOpen=false; closeAvatarPreview(); panel.classList.remove('kc-open'); panel.classList.add('kc-close'); panel.setAttribute('aria-hidden','true'); openBtn.setAttribute('aria-expanded','false'); setTimeout(()=>{ panel.style.display='none'; }, 140); } function toggle(){ isOpen ? closePanel() : openPanel(); } function line(role, contentNode, options){ const opts = (options && typeof options === 'object') ? options : {}; const shouldPersist = opts.persist !== false; const forceScroll = opts.forceScroll === true; const wrap=document.createElement('div'); wrap.className='kc-msg '+(role==='user'?'kc-user':'kc-bot'); const wasNearBottom = isLogNearBottom(); if(role !== 'user' && AVATAR_URL){ const av=document.createElement('img'); av.className='kc-avatar'; av.src=AVATAR_URL; av.alt='avatar'; wrap.appendChild(av); } const b=document.createElement('div'); b.className='kc-bubble'; let copyValue = ''; if(typeof contentNode==='string'){ b.textContent=contentNode; copyValue = contentNode; } else if (contentNode instanceof Node) { b.appendChild(contentNode); copyValue = toPlainText(b.innerHTML); } else if (contentNode !== null && contentNode !== undefined) { const fallbackText = String(contentNode); b.textContent = fallbackText; copyValue = fallbackText; } if(!copyValue && b.textContent){ copyValue = b.textContent; } const trimmedCopy = (typeof copyValue === 'string') ? copyValue.trim() : ''; if(shouldPersist && trimmedCopy){ addHistoryEntry(role, trimmedCopy); } attachCopyControl(b, copyValue); wrap.appendChild(b); appendToLog(wrap); bindAvatarPreview(wrap); const shouldStick = forceScroll || role === 'user' || wasNearBottom; if (shouldStick) { scrollToBottom({ behavior: opts.behavior === 'smooth' ? 'smooth' : 'auto' }); } else { updateScrollState(); } return {wrap,b}; } function formatBotMessage(rawMessage){ const original = typeof rawMessage === 'string' ? rawMessage : ''; const trimmedOriginal = original.trim(); if (!original) { return { kind: 'text', text: '', typingText: '', historyText: '', }; } if (!PARSING_ENABLED) { return { kind: 'text', text: original, typingText: trimmedOriginal, historyText: trimmedOriginal, }; } const parsedNode = kcBuildParsedNode(original, PARSING_FORMAT); if (parsedNode) { const plainFromNode = toPlainText(parsedNode.innerHTML) || trimmedOriginal; return { kind: 'node', node: parsedNode, typingText: plainFromNode, historyText: plainFromNode, raw: original, }; } return { kind: 'text', text: original, typingText: trimmedOriginal, historyText: trimmedOriginal, }; } function maybeSendWelcome(){ if (!WELCOME_ENABLED) return; if (welcomeSent) return; if (history.length > 0) { welcomeSent = true; return; } if (!WELCOME_MESSAGE_TEXT) { welcomeSent = true; return; } const formattedWelcome = formatBotMessage(WELCOME_MESSAGE_TEXT); if (formattedWelcome.kind === 'node' && formattedWelcome.node) { const welcomeLine = line('bot', formattedWelcome.node, { forceScroll: true }); if (welcomeLine && welcomeLine.b) { welcomeLine.b.classList.add('kc-rich'); welcomeLine.b.dataset.kcParsingFormat = PARSING_FORMAT; } } else { const welcomeText = formattedWelcome.kind === 'text' ? formattedWelcome.text : WELCOME_MESSAGE_TEXT; line('bot', welcomeText, { forceScroll: true }); } welcomeSent = true; } function renderBotReply(message){ const raw = typeof message === 'string' ? message : ''; const formatted = formatBotMessage(raw); const historyText = formatted && typeof formatted.historyText === 'string' ? formatted.historyText.trim() : raw.trim(); const typingPayload = formatted && typeof formatted.typingText === 'string' ? formatted.typingText : ''; const hasTypingPayload = typingPayload.trim() !== ''; const shouldUseTyping = (TYPING_MODE === 'wpm') && hasTypingPayload; if (!shouldUseTyping) { cancelTypingAnimation(); const shouldStick = isLogNearBottom(); if (formatted.kind === 'node' && formatted.node) { const bubbleLine = line('bot', formatted.node, { forceScroll: shouldStick }); if (bubbleLine && bubbleLine.b) { bubbleLine.b.classList.add('kc-rich'); bubbleLine.b.dataset.kcParsingFormat = PARSING_FORMAT; } return; } const textToRender = formatted.kind === 'text' ? formatted.text : raw; line('bot', textToRender, { forceScroll: shouldStick }); return; } cancelTypingAnimation(); const shouldAutoStick = isLogNearBottom(); const target = line('bot', '', { persist: false, forceScroll: shouldAutoStick }); if (!target || !target.b) { const fallbackStick = isLogNearBottom(); const fallbackText = formatted.kind === 'text' ? formatted.text : raw; line('bot', fallbackText, { forceScroll: fallbackStick }); return; } const bubble = target.b; const textNode = document.createElement('span'); bubble.appendChild(textNode); const safeWpm = Math.min(Math.max(TYPING_WPM, 100), 5000); const delayPerChar = Math.max(10, Math.round(60000 / (safeWpm * 5))); const typingSource = typingPayload || raw; let index = 0; let finished = false; function finalizeRichBubble(){ if (formatted && formatted.kind === 'node' && formatted.node) { bubble.innerHTML = ''; bubble.appendChild(formatted.node); bubble.classList.add('kc-rich'); bubble.dataset.kcParsingFormat = PARSING_FORMAT; const finalCopyPlain = formatted.historyText || toPlainText(bubble.innerHTML) || typingPayload || raw; attachCopyControl(bubble, finalCopyPlain); } else { const finalText = formatted && formatted.kind === 'text' ? formatted.text : raw; textNode.textContent = finalText; attachCopyControl(bubble, finalText); } } function finish(){ if (finished) return; finished = true; finalizeRichBubble(); if (historyText) { addHistoryEntry('bot', historyText); } typingAnimationHandle = null; if (log) { if (shouldAutoStick) { scrollToBottom({ behavior: 'auto' }); } else { updateScrollState(); } } } function step(){ if (finished) return; if (index >= typingSource.length){ finish(); return; } index += 1; textNode.textContent = typingSource.slice(0, index); if (log) { if (shouldAutoStick) { scrollToBottom({ behavior: 'auto' }); } else { updateScrollState(); } } window.setTimeout(step, delayPerChar); } typingAnimationHandle = { cancel: finish, }; step(); } function migrateLegacyHistory(){ const legacyKeys = []; if (LEGACY_HISTORY_KEY && LEGACY_HISTORY_KEY !== HISTORY_STORAGE_KEY) { legacyKeys.push(LEGACY_HISTORY_KEY); } if (SESSION_HEX && LEGACY_VISITOR_HISTORY_KEY && LEGACY_VISITOR_HISTORY_KEY !== HISTORY_STORAGE_KEY) { legacyKeys.push(LEGACY_VISITOR_HISTORY_KEY); } if(!legacyKeys.length) return; const existing = readHistoryFromStorage(HISTORY_STORAGE_KEY); let aggregatedItems = Array.isArray(existing.items) ? [...existing.items] : []; legacyKeys.forEach((legacyKey) => { const { items: legacyItems, sessionId: storedSessionId } = readHistoryFromStorage(legacyKey); const hasLegacyItems = Array.isArray(legacyItems) && legacyItems.length > 0; const sessionMismatch = SESSION_HEX && storedSessionId && storedSessionId !== SESSION_HEX; const isVisitorBasedKey = legacyKey === LEGACY_VISITOR_HISTORY_KEY; try { localStorage.removeItem(legacyKey); } catch(_){ /* noop */ } if (!hasLegacyItems || sessionMismatch || isVisitorBasedKey) { return; } aggregatedItems = aggregatedItems.concat(legacyItems); }); history = applyHistoryPolicy(aggregatedItems); if(history.length){ persistHistory(); } else { clearHistoryStorage(); } welcomeSent = history.length > 0; } function restoreHistory(){ const stored = readHistoryFromStorage(HISTORY_STORAGE_KEY); if (SESSION_HEX && stored.sessionId && stored.sessionId !== SESSION_HEX) { history = []; clearHistoryStorage(); return; } const sanitized = applyHistoryPolicy(stored.items); if(!sanitized.length){ history = []; clearHistoryStorage(); return; } history = sanitized; persistHistory(); welcomeSent = history.length > 0; history.forEach((entry)=>{ if(entry && entry.text) line(entry.role, entry.text, {persist:false}); }); } function enforceHistoryPolicy(){ ensureCurrentSession(); const stored = readHistoryFromStorage(HISTORY_STORAGE_KEY); if (SESSION_HEX && stored.sessionId && stored.sessionId !== SESSION_HEX) { history = []; clearHistoryStorage(); return; } const filtered = applyHistoryPolicy(stored.items); if(!filtered.length){ history = []; clearHistoryStorage(); return; } const changed = filtered.length !== history.length || filtered.some((entry, idx) => { const current = history[idx]; return !current || entry.text !== current.text || entry.role !== current.role || entry.ts !== current.ts; }); history = filtered; if(changed) persistHistory(); welcomeSent = history.length > 0; } function setupHistoryRetentionWatcher(){ window.addEventListener('beforeunload', enforceHistoryPolicy); document.addEventListener('visibilitychange', () => { if(document.visibilityState === 'hidden') enforceHistoryPolicy(); }); window.setInterval(enforceHistoryPolicy, 5 * 60 * 1000); } migrateLegacyHistory(); restoreHistory(); setupHistoryRetentionWatcher(); let OFFLINE_QUEUE_KEY = `masgar_chat_offline_${VISITOR_ID}_${SESSION_HEX}_${HISTORY_KEY_SUFFIX}`; const MAX_OFFLINE_QUEUE_SIZE = 10; function readOfflineQueueFromStorage(){ if (!OFFLINE_QUEUE_KEY) return []; let raw = null; try { raw = sessionStorage.getItem(OFFLINE_QUEUE_KEY); } catch(_) { raw = null; } if (!raw) return []; try { const parsed = JSON.parse(raw); const source = Array.isArray(parsed) ? parsed : (parsed && Array.isArray(parsed.items) ? parsed.items : []); return source .map((item) => (typeof item === 'string' ? item.trim() : '')) .filter((item) => item !== '') .slice(-MAX_OFFLINE_QUEUE_SIZE); } catch(_){ return []; } } let offlineQueue = readOfflineQueueFromStorage(); function ensureCurrentSession(){ const next = sessionStore.current(); if (next.id === SESSION_ID) return false; if (activeRequestController) activeRequestController.abort(); cancelTypingAnimation(); clearHistoryStorage(); try { sessionStorage.removeItem(OFFLINE_QUEUE_KEY); } catch (_) { /* noop */ } history = []; offlineQueue = []; sessionState = next; SESSION_ID = next.id; SESSION_HEX = next.id.replace(/-/g, '').toLowerCase(); HISTORY_STORAGE_KEY = `masgar_chat_history_${VISITOR_ID}_${SESSION_HEX}_${HISTORY_KEY_SUFFIX}`; LEGACY_HISTORY_KEY = `masgar_chat_history_${SESSION_ID}`; OFFLINE_QUEUE_KEY = `masgar_chat_offline_${VISITOR_ID}_${SESSION_HEX}_${HISTORY_KEY_SUFFIX}`; if (log) log.replaceChildren(); welcomeSent = false; return true; } window.setInterval(ensureCurrentSession, 60000); document.addEventListener('visibilitychange', () => { if (document.visibilityState === 'visible') ensureCurrentSession(); }); function persistOfflineQueue(){ if (!OFFLINE_QUEUE_KEY) return; if (!offlineQueue.length){ try { sessionStorage.removeItem(OFFLINE_QUEUE_KEY); } catch(_){ /* noop */ } return; } const payload = offlineQueue.slice(-MAX_OFFLINE_QUEUE_SIZE); try { sessionStorage.setItem(OFFLINE_QUEUE_KEY, JSON.stringify(payload)); } catch(_){ /* noop */ } } function enqueueOfflineMessage(message){ const trimmed = typeof message === 'string' ? message.trim() : ''; if (!trimmed) return; offlineQueue.push(trimmed); if (offlineQueue.length > MAX_OFFLINE_QUEUE_SIZE){ offlineQueue = offlineQueue.slice(-MAX_OFFLINE_QUEUE_SIZE); } persistOfflineQueue(); } function peekOfflineQueue(){ while (offlineQueue.length){ const first = typeof offlineQueue[0] === 'string' ? offlineQueue[0].trim() : ''; if (first){ offlineQueue[0] = first; return first; } offlineQueue.shift(); } persistOfflineQueue(); return null; } function shiftOfflineQueue(){ if (!offlineQueue.length) return null; const value = offlineQueue.shift(); persistOfflineQueue(); return value; } function hasOfflineQueue(){ return offlineQueue.length > 0; } let offlineStatus = navigator.onLine === false; let isFlushingOfflineQueue = false; function isOnline(){ return navigator.onLine !== false; } function offlineStatusMessage(){ return '⚠️ Koneksi terputus. Menunggu jaringan...'; } function showOfflineNotice(customMessage){ if (!statusEl) return; const message = typeof customMessage === 'string' && customMessage.trim() ? customMessage.trim() : offlineStatusMessage(); statusEl.textContent = message; } function clearStatusIfIdle(){ if (!statusEl) return; if (!isSending && !isFlushingOfflineQueue && !offlineStatus){ statusEl.textContent = ''; } } function handleOfflineStatus(customMessage){ offlineStatus = true; showOfflineNotice(customMessage); } async function flushOfflineQueue(){ ensureCurrentSession(); if (isFlushingOfflineQueue) return; if (!hasOfflineQueue() || !isOnline()){ if (!isOnline()){ handleOfflineStatus(); } return; } isFlushingOfflineQueue = true; try { while (hasOfflineQueue() && isOnline()){ const nextMessage = peekOfflineQueue(); if (!nextMessage){ shiftOfflineQueue(); continue; } const result = await ask(nextMessage, { fromQueue: true, skipUserLog: true }); const delivered = !!(result && result.delivered); const wentOffline = !!(result && result.offline); if (!delivered){ if (result && result.attempted) shiftOfflineQueue(); if (wentOffline){ handleOfflineStatus(); } break; } shiftOfflineQueue(); } } finally { isFlushingOfflineQueue = false; if (isOnline() && !hasOfflineQueue()){ clearStatusIfIdle(); } } } function handleOnlineStatus(){ offlineStatus = false; if (hasOfflineQueue()){ if (statusEl){ statusEl.innerHTML = 'Reconnect…'; } flushOfflineQueue(); } else { clearStatusIfIdle(); } } if (!isOnline()){ handleOfflineStatus(); } else if (hasOfflineQueue()){ flushOfflineQueue(); } window.addEventListener('offline', () => { handleOfflineStatus(); }); window.addEventListener('online', () => { handleOnlineStatus(); }); function showTyping(){ const dots=document.createElement('div'); dots.className='kc-typing'; dots.setAttribute('aria-label','Bot sedang mengetik'); dots.innerHTML=''; return line('bot', dots, {persist:false}); } function setLoadingState(loading, networkMsg){ send.disabled=loading; send.classList.toggle('kc-loading',loading); isSending=loading; statusEl.innerHTML = loading ? `${networkMsg||'Menunggu Jawaban...'}` : ''; } function normaliseReplyValue(value, seen){ const visited = seen || new WeakSet(); if (value === null || value === undefined) return ''; if (typeof value === 'string') return value.trim(); if (typeof value === 'number') return String(value); if (typeof value === 'boolean') return ''; if (Array.isArray(value)){ if (visited.has(value)) return ''; visited.add(value); for (const item of value){ const resolved = normaliseReplyValue(item, visited); if (resolved) return resolved; } return ''; } if (typeof value === 'object'){ if (visited.has(value)) return ''; visited.add(value); const priorityKeys = ['output','reply','message','answer','text','content','data','response','result','choices','fulfillmentText','raw','value','body']; for (const key of priorityKeys){ if (!Object.prototype.hasOwnProperty.call(value, key)) continue; const resolved = normaliseReplyValue(value[key], visited); if (resolved) return resolved; } } return ''; } function resolveReply(payload, fallbackText){ const primary = normaliseReplyValue(payload, new WeakSet()); if (primary) return primary; if (typeof fallbackText === 'string' && fallbackText.trim()){ return fallbackText.trim(); } return ''; } async function ask(msg, options){ ensureCurrentSession(); const requestSessionId = SESSION_ID; const opts = (options && typeof options === 'object') ? options : {}; const fromQueue = !!opts.fromQueue; const skipUserLog = !!opts.skipUserLog; const rawMessage = typeof msg === 'string' ? msg : ''; const userMsg = rawMessage.trim(); if (!userMsg) return { delivered:false, offline:false }; if(!isOpen) openPanel(); if (!fromQueue && userMsg.length > MAX_USER_CHARS) { if (statusEl) { statusEl.textContent = `Pesan terlalu panjang (maks ${MAX_USER_CHARS} karakter).`; } if (input) { input.focus(); } return { delivered:false, offline:false }; } if (!fromQueue && !skipUserLog){ line('user', userMsg); } if (!fromQueue){ input.value=''; } if (!isOnline()){ if (!fromQueue){ enqueueOfflineMessage(userMsg); } handleOfflineStatus(); return { delivered:false, offline:true }; } setLoadingState(true, fromQueue ? 'Reconnect…' : 'Menunggu Jawaban...'); const typing = showTyping(); let pendingStatusMessage = ''; let finalStatusMessage = ''; let usedProxyRefresh = false; let requestDispatched = false; const proxyExpiredFailureMessage = 'Sesi chat sempat kedaluwarsa dan percobaan ulang juga gagal. Silakan kirim ulang pesan anda atau Refresh browser anda.'; const payload = { sessionId: SESSION_HEX, visitorId: VISITOR_ID, action:'sendMessage', chatInput: userMsg }; const meta = { visitorId: VISITOR_ID, sessionId: SESSION_HEX, wpUserId: WP_UID==='0'?null:WP_UID, displayName: WP_NAME||null, pageUrl: location.href, pageTitle: document.title, referrer: document.referrer||null, userAgent: navigator.userAgent, tzOffsetMin: new Date().getTimezoneOffset(), ts: new Date().toISOString(), parsing: { enabled: PARSING_ENABLED, format: PARSING_FORMAT, }, }; const makeFetch = async () => { const body = { proxy_id: proxyId, payload, meta }; if (CONFIG_KEY) { body.config_key = CONFIG_KEY; } requestDispatched = true; const controller = new AbortController(); activeRequestController = controller; const timer = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS); try { const response = await fetch(REST_URL, { method: 'POST', signal: controller.signal, headers: { 'Content-Type':'application/json', 'X-WP-Nonce': REST_NONCE }, body: JSON.stringify(body) }); const rawBody = await response.text(); const trimmedBody = typeof rawBody === 'string' ? rawBody.trim() : ''; let parsedBody = null; if (trimmedBody) { try { parsedBody = JSON.parse(trimmedBody); } catch(_) { parsedBody = rawBody; } } else { parsedBody = rawBody; } return { response, rawBody, trimmedBody, parsedBody }; } finally { clearTimeout(timer); if (activeRequestController === controller) activeRequestController = null; } }; const extractErrorPayload = (result) => { if (!result) return null; const payload = result.parsedBody; if (payload && typeof payload === 'object' && !Array.isArray(payload)) { return payload; } return null; }; const totalAttempts = 1; // POST delivery is ambiguous after a transport failure. const baseDelayMs = 1200; const maxDelayMs = 7000; const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); let conversationAnswered = false; let abortRetries = false; let lastFriendlyError = GENERIC_SERVER_ERROR_MESSAGE; let offlineEncountered = false; let shouldRequeueMessage = false; try{ for (let attempt = 0; attempt < totalAttempts; attempt++) { if (!isOpen) { abortRetries = true; break; } if (attempt > 0) { const attemptNumber = attempt + 1; const waitMs = Math.min(maxDelayMs, Math.round(baseDelayMs * Math.pow(2, attempt - 1))); const retryLabel = fromQueue ? 'Reconnect…' : `Percobaan ulang ${attemptNumber}/${totalAttempts}...`; setLoadingState(true, retryLabel); await sleep(waitMs); if (!isOpen) { abortRetries = true; break; } if (conversationAnswered) break; if (!isOnline()){ offlineEncountered = true; shouldRequeueMessage = !fromQueue && !requestDispatched; pendingStatusMessage = offlineStatusMessage(); break; } } let fetchResult = null; let fetchError = null; let parsedErr = null; try { fetchResult = await makeFetch(); parsedErr = extractErrorPayload(fetchResult); if ( attempt === 0 && fetchResult && fetchResult.response && !fetchResult.response.ok && fetchResult.response.status === 400 ) { const newProxy = parsedErr && typeof parsedErr.proxy_id === 'string' ? parsedErr.proxy_id.trim() : ''; if (!usedProxyRefresh && parsedErr && parsedErr.code === 'proxy_expired' && newProxy) { proxyId = newProxy; usedProxyRefresh = true; fetchResult = await makeFetch(); parsedErr = extractErrorPayload(fetchResult); } } } catch (err) { fetchError = err; console.error('masgar-chatbot: permintaan gagal', err); } if (ensureCurrentSession() || requestSessionId !== SESSION_ID) { abortRetries = true; break; } if (!isOpen && !conversationAnswered) { abortRetries = true; break; } if (fetchResult && fetchResult.response && fetchResult.response.ok) { const { parsedBody, rawBody, trimmedBody } = fetchResult; if (parsedBody && typeof parsedBody === 'object' && (parsedBody.ok === false || parsedBody.success === false)) { const error = parsedBody.error; pendingStatusMessage = (error && typeof error.message === 'string' ? error.message : '') || (typeof error === 'string' ? error : '') || resolveReply(parsedBody, '') || GENERIC_SERVER_ERROR_MESSAGE; break; } let reply = resolveReply(parsedBody, trimmedBody); if (!reply) { console.warn('masgar-chatbot: balasan webhook tidak dapat dibaca', fetchResult.response.status); reply = FALLBACK_RETRY_MESSAGE; } typing?.wrap?.remove(); renderBotReply(reply); statusEl.textContent=''; pendingStatusMessage = ''; conversationAnswered = true; break; } if (fetchError) { if (!isOnline()){ offlineEncountered = true; shouldRequeueMessage = !fromQueue && !requestDispatched; typing?.wrap?.remove(); pendingStatusMessage = offlineStatusMessage(); break; } lastFriendlyError = fetchError.name === 'AbortError' ? 'Waktu tunggu chatbot habis. Silakan coba lagi.' : (usedProxyRefresh ? proxyExpiredFailureMessage : GENERIC_SERVER_ERROR_MESSAGE); if (attempt === totalAttempts - 1) { typing?.wrap?.remove(); pendingStatusMessage = lastFriendlyError; break; } continue; } if (!fetchResult || !fetchResult.response) { if (!isOnline()){ offlineEncountered = true; shouldRequeueMessage = !fromQueue && !requestDispatched; typing?.wrap?.remove(); pendingStatusMessage = offlineStatusMessage(); break; } lastFriendlyError = usedProxyRefresh ? proxyExpiredFailureMessage : GENERIC_SERVER_ERROR_MESSAGE; if (attempt === totalAttempts - 1) { typing?.wrap?.remove(); pendingStatusMessage = lastFriendlyError; break; } continue; } const response = fetchResult.response; const friendlyError = (() => { let message = GENERIC_SERVER_ERROR_MESSAGE; if (parsedErr && !usedProxyRefresh) { const candidateRaw = typeof parsedErr.error === 'string' ? parsedErr.error : (typeof parsedErr.message === 'string' ? parsedErr.message : ''); const candidate = toPlainText(candidateRaw); if (candidate) message = candidate; } if (usedProxyRefresh) { message = proxyExpiredFailureMessage; } return toPlainText(message) || GENERIC_SERVER_ERROR_MESSAGE; })(); lastFriendlyError = friendlyError; const statusCode = response.status; const isRetryable = statusCode >= 500 && statusCode < 600; if (isRetryable && attempt < totalAttempts - 1) { continue; } typing?.wrap?.remove(); pendingStatusMessage = friendlyError; break; } if (!conversationAnswered && !abortRetries && !pendingStatusMessage && !offlineEncountered && lastFriendlyError) { typing?.wrap?.remove(); pendingStatusMessage = lastFriendlyError; } if (abortRetries) { typing?.wrap?.remove(); pendingStatusMessage = ''; statusEl.textContent = ''; } } catch(e){ console.error('masgar-chatbot: permintaan gagal', e); typing?.wrap?.remove(); if (!isOnline()){ offlineEncountered = true; shouldRequeueMessage = !fromQueue && !requestDispatched; pendingStatusMessage = offlineStatusMessage(); } else { pendingStatusMessage = usedProxyRefresh ? proxyExpiredFailureMessage : GENERIC_SERVER_ERROR_MESSAGE; } } finally { typing?.wrap?.remove(); setLoadingState(false); if (!fromQueue){ input.focus(); } if (pendingStatusMessage) { statusEl.textContent = pendingStatusMessage; finalStatusMessage = pendingStatusMessage; pendingStatusMessage = ''; } } if (shouldRequeueMessage && !fromQueue){ enqueueOfflineMessage(userMsg); } if (offlineEncountered){ handleOfflineStatus(finalStatusMessage || undefined); } else if (!finalStatusMessage){ clearStatusIfIdle(); } return { delivered: conversationAnswered, offline: offlineEncountered, attempted: requestDispatched }; } function handleEnter(e){ if(e.key==='Enter' && !e.shiftKey){ e.preventDefault(); if(isSending) return; ask(input.value); } } bindAvatarPreview(document); openBtn.addEventListener('click', toggle); document.addEventListener('keydown', (e)=>{ if(isAvatarViewerOpen()){ closeAvatarPreview(); return; } if(e.key==='Escape' && isOpen) closePanel(); }); input.addEventListener('keydown', handleEnter); send.addEventListener('click', ()=> { if(isSending) return; ask(input.value); }); } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { init(); } })();