1(function () { 2// ===== ã»ãã¥ãªãã£ãã«ãã¼ ===== 3const escapeHtml = (s) => String(s ?? '').replace(/[&<>"']/g, 4 c => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c])); 5const safeUrl = (u) => /^https?:\/\//i.test(u) ? u : '#'; 6 7 'use strict'; 8 9 // CONFIGã¯åæ½è¨ãã¡ã¤ã«ï¼hr-chat-widget-{facility}.jsï¼ã§å®ç¾©ããã㨠10 11 // ===== HTMLæ§ç¯ ===== 12 const quickBtnsHtml = CONFIG.quickBtns 13 .map(b => `<button class="hw-quick-btn" onclick="hwQuick('${b.text}')">${b.label}</button>`) 14 .join('\n '); 15 16 const avatarHtml = CONFIG.avatarImg 17 ? `<img src="${CONFIG.avatarImg}" style="width:20px;height:20px;border-radius:50%;object-fit:cover;">` 18 : CONFIG.avatarLetter; 19 20 const headerAvatarHtml = CONFIG.avatarImg 21 ? `<img src="${CONFIG.avatarImg}" style="width:24px;height:24px;border-radius:50%;object-fit:cover;">` 22 : CONFIG.avatarLetter; 23 24 const html = ` 25 <div id="hw-root"> 26 <div id="hw-bubble">${CONFIG.bubbleMsg}</div> 27 28 <button id="hw-toggle-btn" aria-label="AIã³ã³ã·ã§ã«ã¸ã¥ãéã"> 29 <svg class="icon-chat" viewBox="0 0 24 24" fill="none" stroke="#f5f0e8" stroke-width="1.8"> 30 <path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/> 31 <line x1="9" y1="10" x2="15" y2="10" stroke-linecap="round"/> 32 <line x1="9" y1="13" x2="13" y2="13" stroke-linecap="round"/> 33 </svg> 34 <svg class="icon-close" viewBox="0 0 24 24" fill="none" stroke="#f5f0e8" stroke-width="2"> 35 <line x1="18" y1="6" x2="6" y2="18"/> 36 <line x1="6" y1="6" x2="18" y2="18"/> 37 </svg> 38 </button> 39 40 <div id="hw-panel"> 41 <div class="hw-header"> 42 <div class="hw-avatar">${headerAvatarHtml}</div> 43 <div class="hw-header-info"> 44 <div class="hw-name">${CONFIG.headerName}</div> 45 <div class="hw-status"><div class="hw-dot"></div>ãªã³ã©ã¤ã³</div> 46 </div> 47 <button class="hw-expand-btn" id="hwExpandBtn" onclick="hwToggleExpand()" title="æ¡å¤§è¡¨ç¤º">⤢</button> 48 </div> 49 50 <div class="hw-messages" id="hw-messages"> 51 <div class="hw-msg bot"> 52 <div class="hw-msg-avatar">${avatarHtml}</div> 53 <div class="hw-bubble">${CONFIG.welcomeMsg}</div> 54 </div> 55 </div> 56 57 <div class="hw-typing" id="hw-typing"> 58 <div class="hw-msg-avatar">${avatarHtml}</div> 59 <div class="hw-typing-dots"> 60 <div class="hw-typing-dot"></div> 61 <div class="hw-typing-dot"></div> 62 <div class="hw-typing-dot"></div> 63 </div> 64 </div> 65 66 <div class="hw-quick" id="hw-quick"> 67 ${quickBtnsHtml} 68 </div> 69 70 <div class="hw-input-area"> 71 <textarea class="hw-input" id="hw-input" placeholder="ã質åãã©ãã..." rows="1" 72 onkeydown="hwHandleKey(event)" oninput="hwResize(this)"></textarea> 73 <button class="hw-send" id="hw-send" onclick="hwSend()"> 74 <svg viewBox="0 0 24 24"><line x1="22" y1="2" x2="11" y2="13"/><polygon points="22 2 15 22 11 13 2 9 22 2"/></svg> 75 </button> 76 </div> 77 </div> 78 </div> 79 `; 80 81 const wrapper = document.createElement('div'); 82 wrapper.innerHTML = html; 83 document.body.appendChild(wrapper); 84 85 // ===== ç¶æ ===== 86 let hwHistory = []; 87 let hwLoading = false; 88 let hwBubbleHidden = false; 89 let hwMsgCount = 0; 90 91 // ===== LocalStorage ===== 92 const hwStorageKey = 'hw_history_' + CONFIG.facilityId; 93 const hwStorageTTL = 24 * 60 * 60 * 1000; // 24æé 94 95 function hwSaveHistory() { 96 try { 97 localStorage.setItem(hwStorageKey, JSON.stringify({ 98 ts: Date.now(), 99 sessionId: hwSessionId, 100 history: hwHistory, 101 })); 102 } catch (e) { /* 容éè¶ éçã¯ç¡è¦ */ } 103 } 104 105 function hwLoadHistory() { 106 try { 107 const raw = localStorage.getItem(hwStorageKey); 108 if (!raw) return null; 109 const data = JSON.parse(raw); 110 if (Date.now() - data.ts > hwStorageTTL) { 111 localStorage.removeItem(hwStorageKey); 112 return null; 113 } 114 return data.history; 115 } catch (e) { return null; } 116 } 117 118 // ã»ãã·ã§ã³IDã¯ä¿åæ¸ã¿å±¥æ´ãããã°å¼ãç¶ãããªããã°æ°è¦çæ 119 const _savedRaw = localStorage.getItem(hwStorageKey); 120 const _savedData = (() => { try { return _savedRaw ? JSON.parse(_savedRaw) : null; } catch { return null; } })(); 121 const hwSessionId = (_savedData && _savedData.sessionId) 122 ? _savedData.sessionId 123 : 'sess_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9); 124 125 // ===== å±¥æ´å¾©å ===== 126 (function hwRestoreHistory() { 127 const saved = hwLoadHistory(); 128 if (!saved || saved.length === 0) return; 129 130 // ã¦ã§ã«ã«ã ã¡ãã»ã¼ã¸ã䏿¦ã¯ãªã¢ãã¦å±¥æ´ãåæç» 131 const container = document.getElementById('hw-messages'); 132 container.innerHTML = ''; 133 134 saved.forEach(msg => { 135 hwHistory.push(msg);
136 // DOMåæç»ï¼hwAddMsgã¯ãã£ã¼ãããã¯ãã¿ã³çãä»ããã®ã§ç´æ¥æç»ï¼ 137 const div = document.createElement('div'); 138 div.className = `hw-msg ${msg.role === 'user' ? 'user' : 'bot'}`; 139 140 const avatar = document.createElement('div'); 141 avatar.className = 'hw-msg-avatar'; 142 if (msg.role !== 'user') { 143 if (CONFIG.avatarImg) { 144 avatar.innerHTML = `<img src="${CONFIG.avatarImg}" style="width:20px;height:20px;border-radius:50%;object-fit:cover;">`; 145 } else { 146 avatar.textContent = CONFIG.avatarLetter; 147 avatar.style.cssText = `background:${CONFIG.themeColor};color:${CONFIG.accentColor};font-size:10px;`; 148 } 149 } else { 150 avatar.textContent = 'ð¤'; 151 } 152 153 const bubble = document.createElement('div'); 154 bubble.className = 'hw-bubble'; 155 let t = escapeHtml(msg.content) 156 157 t = t.replace( 158 /https?:\/\/[^\s\u3000-\u9fff\uff00-\uffefããï¼ï¼ããï¼ï¼ããããããâ¦â]+/g, 159 url => { 160 url = url.replace(/[\s.,;:!?ï¼)>\]ãã]+$/, ''); 161 return `<a href="${safeUrl(url)}" target="_blank" rel="noopener" style="color:${CONFIG.themeColor};word-break:break-all;text-decoration:underline;">ãã¡ã</a>`; 162 } 163 ); 164 bubble.innerHTML = t.replace(/\n/g, '<br>'); 165 166 div.appendChild(avatar); 167 div.appendChild(bubble); 168 container.appendChild(div); 169 }); 170 171 // ã¯ã¤ãã¯ãã¿ã³ãé表示 172 document.getElementById('hw-quick').style.display = 'none'; 173 container.scrollTop = container.scrollHeight; 174 })(); 175 176 // ===== éé ===== 177 document.getElementById('hw-toggle-btn').addEventListener('click', function () { 178 const panel = document.getElementById('hw-panel'); 179 const bubble = document.getElementById('hw-bubble'); 180 panel.classList.toggle('open'); 181 this.classList.toggle('open'); 182 if (!hwBubbleHidden) { 183 bubble.style.display = 'none'; 184 hwBubbleHidden = true; 185 } 186 }); 187 188 document.getElementById('hw-bubble').addEventListener('click', function () { 189 document.getElementById('hw-panel').classList.add('open'); 190 document.getElementById('hw-toggle-btn').classList.add('open'); 191 this.style.display = 'none'; 192 hwBubbleHidden = true; 193 }); 194 195 // ããã«ã5ç§å¾ã«èªåé表示 196 setTimeout(() => { 197 const bubble = document.getElementById('hw-bubble'); 198 if (bubble && !hwBubbleHidden) { 199 bubble.style.animation = 'none'; 200 bubble.style.opacity = '0'; 201 bubble.style.transition = 'opacity 0.5s'; 202 setTimeout(() => { if (!hwBubbleHidden) bubble.style.display = 'none'; }, 500); 203 } 204 }, 5000); 205 206 // ===== æ¡å¤§ã»ç¸®å° ===== 207 window.hwToggleExpand = function () { 208 const panel = document.getElementById('hw-panel'); 209 const btn = document.getElementById('hwExpandBtn'); 210 const isExpanded = panel.classList.contains('expanded'); 211 panel.classList.toggle('expanded'); 212 btn.textContent = isExpanded ? '⤢' : '⤡'; 213 btn.title = isExpanded ? 'æ¡å¤§è¡¨ç¤º' : '縮å°è¡¨ç¤º'; 214 setTimeout(() => { 215 const msgs = document.getElementById('hw-messages'); 216 if (msgs) msgs.scrollTop = msgs.scrollHeight; 217 }, 300); 218 }; 219 220 // ===== å ¥åè£å© ===== 221 window.hwResize = function (el) { 222 el.style.height = 'auto'; 223 el.style.height = Math.min(el.scrollHeight, 100) + 'px'; 224 }; 225 226 window.hwHandleKey = function (e) { 227 if (e.key === 'Enter' && !e.shiftKey) { 228 e.preventDefault(); 229 hwSend(); 230 } 231 }; 232 233 window.hwQuick = function (text) { 234 document.getElementById('hw-input').value = text; 235 hwSend(); 236 }; 237 238 // ===== ã¡ãã»ã¼ã¸è¿½å ===== 239 function hwAddMsg(role, content) { 240 const container = document.getElementById('hw-messages'); 241 242 const div = document.createElement('div'); 243 div.className = `hw-msg ${role}`; 244 245 const avatar = document.createElement('div'); 246 avatar.className = 'hw-msg-avatar'; 247 if (role === 'bot') { 248 if (CONFIG.avatarImg) { 249 avatar.innerHTML = `<img src="${CONFIG.avatarImg}" style="width:20px;height:20px;border-radius:50%;object-fit:cover;">`; 250 avatar.style.cssText = ''; 251 } else { 252 avatar.textContent = CONFIG.avatarLetter; 253 avatar.style.cssText = `background:${CONFIG.themeColor};color:${CONFIG.accentColor};font-size:10px;`; 254 } 255 } else { 256 avatar.textContent = 'ð¤'; 257 } 258 259 const bubble = document.createElement('div'); 260 bubble.className = 'hw-bubble'; 261 bubble.innerHTML = (() => { 262 let t = escapeHtml(content) 263 264 t = t.replace( 265 /https?:\/\/[^\s\u3000-\u9fff\uff00-\uffefããï¼ï¼ããï¼ï¼ããããããâ¦â]+/g, 266 url => { 267 url = url.replace(/[\s.,;:!?ï¼)>\]ãã]+$/, ''); 268 return `<a href="${safeUrl(url)}" target="_blank" rel="noopener" style="color:${CONFIG.themeColor};word-break:break-all;text-decoration:underline;">ãã¡ã</a>`; 269 } 270 ); 271 return t.replace(/\n/g, '<br>'); 272 })(); 273 274 div.appendChild(avatar); 275 div.appendChild(bubble); 276 container.appendChild(div); 277 278 // AIã¡ãã»ã¼ã¸ã«ãã£ã¼ãããã¯ãã¿ã³ã追å 279 if (role === 'bot') hwAttachFeedback(div); 280 281 container.scrollTop = container.scrollHeight; 282 } 283 284 // ===== ãã£ã¼ãããã¯ãã¿ã³è¿½å ï¼å ±éï¼ ===== 285 // hwAddMsg / hwSendï¼ã¹ããªã¼ãã³ã°ï¼ã®ä¸¡ãã¹ããå¼ã¶ãã¨ã§ãã°ã¬ãé²ã 286 function hwAttachFeedback(msgDiv) { 287 hwMsgCount++;
288 const msgId = 'msg_' + hwMsgCount; 289 msgDiv.dataset.msgId = msgId; 290 291 const fbDiv = document.createElement('div'); 292 fbDiv.className = 'hw-feedback'; 293 294 const goodBtn = document.createElement('button'); 295 goodBtn.className = 'hw-fb-btn'; 296 goodBtn.title = 'å½¹ã«ç«ã£ã'; 297 goodBtn.textContent = 'ð'; 298 goodBtn.onclick = function () { 299 const content = msgDiv.querySelector('.hw-bubble')?.innerText || ''; 300 hwFeedback(msgId, 1, this, content); 301 }; 302 303 const badBtn = document.createElement('button'); 304 badBtn.className = 'hw-fb-btn'; 305 badBtn.title = 'æ¹åãå¿ è¦'; 306 badBtn.textContent = 'ð'; 307 badBtn.onclick = function () { 308 const content = msgDiv.querySelector('.hw-bubble')?.innerText || ''; 309 hwFeedback(msgId, -1, this, content); 310 }; 311 312 fbDiv.appendChild(goodBtn); 313 fbDiv.appendChild(badBtn); 314 document.getElementById('hw-messages').appendChild(fbDiv); 315 316 document.getElementById('hw-messages').scrollTop = 99999; 317 } 318 319 // ===== ãã£ã¼ãããã¯éä¿¡ ===== 320 async function hwFeedback(msgId, value, btnEl, messageContent) { 321 btnEl.parentElement.querySelectorAll('.hw-fb-btn').forEach(b => b.classList.remove('active-good', 'active-bad')); 322 btnEl.classList.add(value === 1 ? 'active-good' : 'active-bad'); 323 try { 324 await fetch(CONFIG.workerUrl + '/feedback', { 325 method: 'POST', 326 headers: { 'Content-Type': 'application/json' }, 327 body: JSON.stringify({ 328 facility_id: CONFIG.facilityId, 329 session_id: hwSessionId, 330 type: 'message', 331 value, 332 message_content: (messageContent || '').slice(0, 500), 333 }), 334 }); 335 } catch (e) { console.warn('ãã£ã¼ãããã¯é信失æ:', e); } 336 } 337 338 // ===== æºè¶³åº¦ããã«è¡¨ç¤º ===== 339 // ===== ã¿ã¤ãã³ã°è¡¨ç¤º ===== 340 function hwSetTyping(show) { 341 const t = document.getElementById('hw-typing'); 342 t.classList.toggle('show', show); 343 if (show) document.getElementById('hw-messages').scrollTop = 99999; 344 } 345 346 // ===== ã¡ãã»ã¼ã¸éä¿¡ ===== 347 window.hwSend = async function () { 348 if (hwLoading) return; 349 350 const input = document.getElementById('hw-input'); 351 const text = input.value.trim(); 352 if (!text) return; 353 354 hwAddMsg('user', text); 355 hwHistory.push({ role: 'user', content: text }); 356 hwSaveHistory(); 357 358 input.value = ''; 359 input.style.height = 'auto'; 360 document.getElementById('hw-quick').style.display = 'none'; 361 document.getElementById('hw-send').disabled = true; 362 hwLoading = true; 363 hwSetTyping(true); 364 365 try { 366 const res = await fetch(CONFIG.workerUrl + '/chat', { 367 method: 'POST', 368 headers: { 'Content-Type': 'application/json' }, 369 body: JSON.stringify({ 370 facility_id: CONFIG.facilityId, 371 session_id: hwSessionId, 372 messages: hwHistory, 373 }), 374 }); 375 376 if (!res.ok) { 377 const err = await res.json(); 378 throw new Error(err.error || 'APIã¨ã©ã¼'); 379 } 380 381 // ã¹ããªã¼ãã³ã°åãåã 382 hwSetTyping(false); 383 const msgDiv = document.createElement('div'); 384 msgDiv.className = 'hw-msg bot'; 385 const avatar = document.createElement('div'); 386 avatar.className = 'hw-msg-avatar'; 387 if (CONFIG.avatarImg) { 388 avatar.innerHTML = `<img src="${CONFIG.avatarImg}" style="width:20px;height:20px;border-radius:50%;object-fit:cover;">`; 389 avatar.style.cssText = ''; 390 } else { 391 avatar.textContent = CONFIG.avatarLetter; 392 avatar.style.cssText = `background:${CONFIG.themeColor};color:${CONFIG.accentColor};font-size:10px;`; 393 } 394 const bubble = document.createElement('div'); 395 bubble.className = 'hw-bubble'; 396 bubble.innerHTML = ''; 397 msgDiv.appendChild(avatar); 398 msgDiv.appendChild(bubble); 399 document.getElementById('hw-messages').appendChild(msgDiv); 400 401 const reader = res.body.getReader(); 402 const decoder = new TextDecoder('utf-8', { fatal: false, ignoreBOM: true }); 403 let fullText = ''; 404 let sourceUrl = null; 405 let buffer = ''; 406 407 while (true) { 408 const { done, value } = await reader.read(); 409 if (done) break; 410 buffer += decoder.decode(value, { stream: true }); 411 const lines = buffer.split('\n'); 412 buffer = lines.pop(); 413 for (const line of lines) { 414 if (!line.startsWith('data: ')) continue; 415 const data = line.slice(6).trim(); 416 try { 417 const parsed = JSON.parse(data); 418 if (parsed.delta) { 419 fullText += parsed.delta; 420 bubble.innerHTML = escapeHtml(fullText).replace( 421 /https?:\/\/[^\s\u3000-\u9fff\uff00-\uffefããï¼ï¼ããï¼ï¼ããããããâ¦â]+/g, 422 url => { 423 url = url.replace(/[\s.,;:!?ï¼)>\]ãã]+$/, ''); 424 return `<a href="${safeUrl(url)}" target="_blank" rel="noopener" style="color:${CONFIG.themeColor};word-break:break-all;text-decoration:underline;">ãã¡ã</a>`; 425 } 426 ).replace(/\n/g, '<br>'); 427 document.getElementById('hw-messages').scrollTop = 99999; 428 } 429 if (parsed.done) { 430 sourceUrl = parsed.sourceUrl || null; 431 } 432 } catch {} 433 } 434 } 435 436 if (sourceUrl) { 437 bubble.innerHTML += '<br><br>' + `<a href="${safeUrl(sourceUrl)}" target="_blank" rel="noopener" style="display:inline-block;margin-top:6px;font-size:12px;color:${CONFIG.themeColor};text-decoration:underline;">ð åèãã¼ã¸</a>`; 438 } 439 440 hwHistory.push({ role: 'assistant', content: fullText }); 441 hwSaveHistory(); 442 hwAttachFeedback(msgDiv); 443 444 } catch (e) { 445 hwSetTyping(false); 446 hwAddMsg('bot', `ç³ã訳ãããã¾ãããã¨ã©ã¼ãçºçãããã¾ããã\n${CONFIG.errorPhone ? 'ãé»è©±ï¼' + CONFIG.errorPhone + 'ï¼ã«ã¦ãåãåãããã ããã' : 'ãåãåãããã ããã'}`); 447 } finally {
448 document.getElementById('hw-send').disabled = false; 449 hwLoading = false; 450 } 451 }; 452 453})();
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.