|
|
@@ -69,7 +69,7 @@
|
|
|
.brand-card-arrow{float:right;font-size:13px;color:#c4a035;margin-left:8px}
|
|
|
.brand-more-btn{display:block;width:100%;padding:8px;margin-top:6px;background:#fffdf5;border:1px dashed #e0c080;border-radius:8px;font-size:12px;color:#b8860b;cursor:pointer;text-align:center;transition:all .15s}
|
|
|
.brand-more-btn:hover{background:#fff8e1;border-color:#d4a017}
|
|
|
- .brand-detail-overlay{position:fixed;inset:0;background:rgba(0,0,0,.4);display:flex;align-items:center;justify-content:center;z-index:200;padding:20px}
|
|
|
+ .brand-detail-overlay{position:fixed;top:0;right:0;bottom:0;left:0;background:rgba(0,0,0,.4);display:flex;align-items:center;justify-content:center;z-index:200;padding:20px}
|
|
|
.brand-detail-box{background:#fff;border-radius:14px;max-width:420px;width:100%;max-height:85vh;overflow-y:auto;box-shadow:0 8px 32px rgba(0,0,0,.18)}
|
|
|
.brand-detail-header{position:sticky;top:0;background:#fff;padding:16px 18px 10px;border-bottom:1px solid #eee;z-index:1}
|
|
|
.brand-detail-header h2{font-size:17px;color:var(--bk);margin:0 0 4px}
|
|
|
@@ -123,7 +123,7 @@
|
|
|
.pager button:disabled{opacity:.35;cursor:default}
|
|
|
.pager .page-info{font-size:12px;color:#888;margin:0 4px;white-space:nowrap}
|
|
|
.pager select{padding:6px 8px;appearance:auto}
|
|
|
- .login-modal-mask{position:fixed;inset:0;display:flex;align-items:center;justify-content:center;padding:24px;background:rgba(0,0,0,.5);z-index:100}
|
|
|
+ .login-modal-mask{position:fixed;top:0;right:0;bottom:0;left:0;display:flex;align-items:center;justify-content:center;padding:24px;background:rgba(0,0,0,.5);z-index:100}
|
|
|
.login-modal{width:min(320px,100%);overflow:hidden;border-radius:12px;background:#fff;box-shadow:0 12px 36px rgba(0,0,0,.2);text-align:center}
|
|
|
.login-modal-title{padding:24px 24px 8px;color:var(--text);font-size:17px;font-weight:700}
|
|
|
.login-modal-desc{padding:0 24px 24px;color:var(--sub);font-size:14px;line-height:1.6}
|
|
|
@@ -264,11 +264,11 @@
|
|
|
h=h.replace(/\[([^\]]+)\]\((https?:\/\/[^\s<>)]+)\)/g,'<a href="$2" target="_blank" rel="noopener" class="ext-link">$1</a>');
|
|
|
h=h.replace(/——— 原文参考 ———/g,"<h3>📖 原文参考</h3>");
|
|
|
var s='(?:结论|详细说明|注意事项|来源明细|适应症|用法与用量|用法用量|禁忌|不良反应|副作用|药理|药物相互作用|贮藏|特殊人群|安全提醒|就医指征|非药物建议|用药建议|用药方案|通用药学知识|来源汇总|病情评估|处理方案|免责声明|AI 声明|AI声明|来源说明)';
|
|
|
- h=h.replace(new RegExp('(?<![【])】\\s*('+s+')','g'),'【$1】');
|
|
|
- h=h.replace(new RegExp('(?<![【])('+s+')】','g'),'【$1】');
|
|
|
+ h=h.replace(new RegExp('】\\s*('+s+')','g'),function(m,name,off,whole){if(off>0&&whole.charAt(off-1)==='【')return m;return'【'+name+'】'});
|
|
|
+ h=h.replace(new RegExp('('+s+')】','g'),function(m,name,off,whole){if(off>0&&whole.charAt(off-1)==='【')return m;return'【'+name+'】'});
|
|
|
// 去掉"来源引用"括号内多余的【栏目】标记,避免被当成栏目标题拆行
|
|
|
for(var k=0;k<3;k++){var _p=h;h=h.replace(/((来源:[^)]*)【[^】]*?】([^)]*))/g,'$1$2');if(h===_p)break;}
|
|
|
- h=h.replace(/(?<![<br>\n])【(.+?)】/g,'<br>【$1】');
|
|
|
+ h=h.replace(/【(.+?)】/g,function(m,inner,off,whole){if(off===0)return'<br>'+m;var pre=whole.charAt(off-1);if(pre==='<'||pre==='b'||pre==='r'||pre==='>'||pre==='\n')return m;return'<br>'+m});
|
|
|
h=h.replace(/\n\n+/g,'</p><p>');h=h.replace(/\n/g,'<br>');
|
|
|
// LLM prompts 产生的栏目标题(大标题样式)
|
|
|
var PROMPT_SECTIONS='结论|详细说明|详细信息|注意事项|来源明细|适应症|用法与用量|用法用量|禁忌|不良反应|副作用|药理|药代动力学|药物相互作用|贮藏|特殊人群|安全提醒|就医指征|非药物建议|用药建议|用药方案|通用药学知识|来源汇总|病情评估|处理方案|免责声明|AI 声明|AI声明|来源说明|记忆技巧|考试频率|知识要点|考点定位|摘要|原文引用|条款出处|关联条款|安全提醒|详细内容|来源说明|药物概况';
|
|
|
@@ -284,7 +284,7 @@
|
|
|
h=h.replace(/(?:<br>|^)(\d+)\.\s+(.+?)(?=<br>|$)/g,'<li>$2</li>');
|
|
|
h=h.replace(/((?:<li>.*?<\/li>)+)/g,function(m){return m.indexOf('<ul>')>=0?m:'<ol>'+m+'</ol>'});
|
|
|
h='<p>'+h+'</p>';h=h.replace(/<p>(<br>)*<\/p>/g,'');h=h.replace(/<p><\/p>/g,'');
|
|
|
- h=h.replace(/(?<!href=")(?<!href=')(?<!">)(?<!'>)(https?:\/\/[^\s<>]+)/g,'<a href="$1" target="_blank" rel="noopener" class="ext-link">$1</a>');
|
|
|
+ h=h.replace(/https?:\/\/[^\s<>]+/g,function(url,off,whole){var pre=whole.substring(Math.max(0,off-6),off);if(/(href=["']|["']>)$/.test(pre))return url;return'<a href="'+url+'" target="_blank" rel="noopener" class="ext-link">'+url+'</a>'});
|
|
|
h=h.replace(/——— 原文参考 ———/g,"<h3>📖 原文参考</h3>");
|
|
|
return h;
|
|
|
}
|
|
|
@@ -455,9 +455,10 @@
|
|
|
body=JSON.stringify({message:query,request_id:metrics.reqId});
|
|
|
}
|
|
|
try{
|
|
|
- var ctrl=streamAbort?streamAbort:new AbortController();
|
|
|
var hdrs=authHeaders();if(metrics&&metrics.reqId)hdrs['X-Request-Id']=metrics.reqId;
|
|
|
- var r=await fetch(endpoint,{method:'POST',headers:hdrs,body:body,signal:ctrl.signal});
|
|
|
+ var fetchOpts={method:'POST',headers:hdrs,body:body};
|
|
|
+ if(streamAbort&&streamAbort.signal!==undefined)fetchOpts.signal=streamAbort.signal;
|
|
|
+ var r=await fetch(endpoint,fetchOpts);
|
|
|
if(r.status===401){console.warn('需要登录');aiDiv.classList.remove('streaming');STREAMING=false;resetSendBtn();contentDiv.innerHTML=md2html('请先登录后再提问');trackComplete(metrics,'error',{http_status:401});return}
|
|
|
if(r.status===429){
|
|
|
var detail='请求过于频繁,请稍后再试';
|
|
|
@@ -467,11 +468,20 @@
|
|
|
if(!r.ok){contentDiv.innerHTML=md2html('请求失败: HTTP '+r.status);aiDiv.classList.remove('streaming');STREAMING=false;resetSendBtn();trackComplete(metrics,'error',{http_status:r.status});return}
|
|
|
sourcesData=[];
|
|
|
brandRecsData=[];
|
|
|
- var reader=r.body.getReader(),decoder=new TextDecoder(),buffer='',fullText='',eventType='';
|
|
|
+ var buffer='',fullText='',eventType='';
|
|
|
+ var reader;
|
|
|
+ if(r.body&&typeof r.body.getReader==='function'&&window.TextDecoder){
|
|
|
+ reader=r.body.getReader();
|
|
|
+ }else{
|
|
|
+ var _all=await r.text(),_used=false;
|
|
|
+ reader={read:function(){if(_used)return Promise.resolve({done:true});_used=true;return Promise.resolve({done:false,value:_all})}};
|
|
|
+ }
|
|
|
+ var decoder=window.TextDecoder?new TextDecoder():null;
|
|
|
while(true){
|
|
|
- var{ done, value }=await reader.read();
|
|
|
- if(done)break;
|
|
|
- buffer+=decoder.decode(value,{stream:true});
|
|
|
+ var chunk=await reader.read();
|
|
|
+ if(chunk.done)break;
|
|
|
+ var value=chunk.value;
|
|
|
+ buffer+=(typeof value==='string')?value:decoder.decode(value,{stream:true});
|
|
|
var lines=buffer.split('\n');buffer=lines.pop()||'';
|
|
|
for(var i=0;i<lines.length;i++){
|
|
|
var line=lines[i].trim();
|
|
|
@@ -574,7 +584,7 @@
|
|
|
doSend(text,hasMedia?pendingMedia:null,reqId);
|
|
|
};
|
|
|
function doSend(text,media,reqId){
|
|
|
- STREAMING=true;streamAbort=new AbortController();
|
|
|
+ STREAMING=true;streamAbort=window.AbortController?new AbortController():null;
|
|
|
document.getElementById('sendBtn').disabled=true;document.getElementById('stopBtn').style.display='inline-block';
|
|
|
var w=document.getElementById('chat').querySelector('.welcome');if(w)w.remove();
|
|
|
var userDisplay=text;
|
|
|
@@ -863,9 +873,11 @@
|
|
|
};
|
|
|
window.switchTab=function(tab){
|
|
|
track('switch_tab', {tab: tab});
|
|
|
- document.querySelectorAll('.nav-btn').forEach(function(b){b.classList.toggle('active',b.dataset.tab===tab)});
|
|
|
- document.getElementById('tabChat').classList.toggle('hidden',tab!=='chat');
|
|
|
- document.getElementById('tabDrugs').classList.toggle('hidden',tab!=='drugs');
|
|
|
+ var btns=document.querySelectorAll('.nav-btn');
|
|
|
+ Array.prototype.forEach.call(btns,function(b){var active=b.getAttribute('data-tab')===tab;if(active){b.classList.add('active')}else{b.classList.remove('active')}});
|
|
|
+ var tabChat=document.getElementById('tabChat'),tabDrugs=document.getElementById('tabDrugs');
|
|
|
+ if(tab==='chat'){tabChat.classList.remove('hidden');tabDrugs.classList.add('hidden')}
|
|
|
+ else{tabChat.classList.add('hidden');tabDrugs.classList.remove('hidden')}
|
|
|
if(tab==='drugs'){
|
|
|
drugKeyword='';drugPage=1;lastSearched='';
|
|
|
showDrugList();
|
|
|
@@ -877,7 +889,7 @@
|
|
|
// 初始化:打开本地数据库 → 恢复对话 → 预加载药品列表 → 加载快捷提问标签
|
|
|
(async function init(){
|
|
|
track('page_view');
|
|
|
- await openChatDB();
|
|
|
+ try{await openChatDB()}catch(e){chatDB=null}
|
|
|
await restoreChat();
|
|
|
loadQuickAsks();
|
|
|
})();
|