|
|
@@ -100,7 +100,7 @@ body{font-family:-apple-system,BlinkMacSystemFont,"PingFang SC","Microsoft YaHei
|
|
|
<div class="status-bar" id="statusBar">● 连接中...</div>
|
|
|
|
|
|
<div id="tabChat" class="tab-content">
|
|
|
- <div class="chat" id="chat"><div class="welcome"><div class="icon"></div><p>输入药品名称或用药问题<br><small style="color:#aaa">1184 种药品 · 4746 条知识片段</small></p></div></div>
|
|
|
+ <div class="chat" id="chat"><div class="welcome"><div class="icon"></div><p>输入药品名称或用药问题<br><small style="color:#aaa" id="statsInfo">加载中...</small></p></div></div>
|
|
|
<div class="tags"><span class="tag-btn" onclick="quickAsk('阿莫西林禁忌')">阿莫西林禁忌</span><span class="tag-btn" onclick="quickAsk('布洛芬用法用量')">布洛芬用法用量</span><span class="tag-btn" onclick="quickAsk('二甲双胍不良反应')">二甲双胍不良反应</span><span class="tag-btn" onclick="quickAsk('青霉素过敏处理')">青霉素过敏处理</span><span class="tag-btn" onclick="quickAsk('吗啡药理作用')">吗啡药理作用</span><span class="tag-btn" onclick="quickAsk('疫苗保存条件')">疫苗保存条件</span></div>
|
|
|
<div class="queue" id="queue" style="display:none"><div id="queueItems"></div></div>
|
|
|
<div class="input-area"><input id="userInput" placeholder="输入药品问题..." onkeydown="if(event.key==='Enter'&&!event.shiftKey){event.preventDefault();send()}"><button class="btn btn-danger" id="sendBtn" onclick="send()">发送</button><button class="btn btn-secondary" id="stopBtn" onclick="stopAll()" style="display:none">⏹ 停止</button></div>
|
|
|
@@ -108,7 +108,7 @@ body{font-family:-apple-system,BlinkMacSystemFont,"PingFang SC","Microsoft YaHei
|
|
|
|
|
|
<div id="tabDrugs" class="tab-content hidden">
|
|
|
<div class="drug-search-bar"><input id="drugSearch" placeholder="搜索药品名称..." onkeydown="if(event.key==='Enter')searchDrugs(1)"><button class="btn btn-danger" onclick="searchDrugs(1)">搜索</button></div>
|
|
|
- <div class="drug-list" id="drugList"><div class="welcome"><div class="icon"></div><p>搜索药典收录的 1184 种药品</p></div></div>
|
|
|
+ <div class="drug-list" id="drugList"><div class="welcome"><div class="icon"></div><p>搜索药典收录的 <span id="drugCount">--</span> 种药品</p></div></div>
|
|
|
<div class="pagination hidden" id="drugPager"></div>
|
|
|
</div>
|
|
|
|
|
|
@@ -122,7 +122,7 @@ body{font-family:-apple-system,BlinkMacSystemFont,"PingFang SC","Microsoft YaHei
|
|
|
|
|
|
<script>
|
|
|
(function(){
|
|
|
-var API=(function(){var o=window.location.origin;if(o&&o!=='null'&&!o.startsWith('file')&&o!=='http://localhost:8080')return o;return'http://'+window.location.hostname+':8080'})();
|
|
|
+var API=(function(){var o=window.location.origin;if(o&&o!=='null'&&!o.startsWith('file'))return o;return'http://'+window.location.hostname+':42158'})();
|
|
|
var TOKEN='',ABORT=null,STREAMING=false,CID='',DRUG_PAGE=1,MQ=[];
|
|
|
|
|
|
function setStatus(m,c){document.getElementById('statusBar').textContent=m;document.getElementById('statusBar').className='status-bar status-'+c}
|
|
|
@@ -168,7 +168,17 @@ async function init(){
|
|
|
setStatus('● 连接后端...','warn');
|
|
|
try{
|
|
|
var r=await fetch(API+'/health');var d=await r.json();
|
|
|
- setStatus('● '+d.app+' '+d.env+' | 1184种药品','ok');
|
|
|
+ setStatus('● '+d.app+' '+d.env,'ok');
|
|
|
+ // 动态获取统计数据
|
|
|
+ try{
|
|
|
+ var sr=await fetch(API+'/api/v1/admin/stats');
|
|
|
+ var sd=await sr.json();
|
|
|
+ if(sd.total_drugs){
|
|
|
+ document.getElementById('statsInfo').textContent=sd.total_drugs+' 种药品 · '+sd.total_chunks+' 条知识片段';
|
|
|
+ document.getElementById('drugCount').textContent=sd.total_drugs;
|
|
|
+ setStatus('● '+d.app+' '+d.env+' | '+sd.total_drugs+'种药品','ok');
|
|
|
+ }
|
|
|
+ }catch(ex){}
|
|
|
try{
|
|
|
var lr=await fetch(API+'/api/v1/auth/guest',{method:'POST',headers:{'Content-Type':'application/json'}});
|
|
|
var ld=await lr.json();if(ld.access_token)TOKEN=ld.access_token;
|
|
|
@@ -207,23 +217,45 @@ async function processQ(){
|
|
|
addMsg('user',text);
|
|
|
|
|
|
var aiDiv=addMsg('ai','',true,''),contentDiv=aiDiv.querySelector('.content');
|
|
|
+ contentDiv.innerHTML='<div class="thinking"><span class="think-dot"></span> 检索中...</div>';
|
|
|
ABORT=new AbortController();
|
|
|
try{
|
|
|
- var r=await fetch(API+'/api/v1/chat/stream',{method:'POST',signal:ABORT.signal,headers:{'Content-Type':'application/json','Authorization':'Bearer '+TOKEN},body:JSON.stringify({message:text,conversation_id:CID||undefined})});
|
|
|
+ var r=await fetch(API+'/api/v1/chat/ask',{method:'POST',signal:ABORT.signal,headers:{'Content-Type':'application/json','Authorization':'Bearer '+TOKEN},body:JSON.stringify({message:text,conversationId:CID||undefined})});
|
|
|
if(!r.ok)throw new Error('HTTP '+r.status);
|
|
|
- var reader=r.body.getReader(),dec=new TextDecoder(),buf='',ev='',out='';
|
|
|
- while(true){var v=await reader.read();if(v.done)break;buf+=dec.decode(v.value,{stream:true});var lines=buf.split('\n');buf=lines.pop()||'';
|
|
|
- for(var i=0;i<lines.length;i++){var l=lines[i];if(l.startsWith('event:')){ev=l.slice(7).trim();continue}if(!l.startsWith('data:'))continue;var d=l.slice(6);if(d==='[DONE]')continue;
|
|
|
- if(ev==='intent'){var tagEl=aiDiv.querySelector('.tag');if(tagEl){tagEl.textContent=intentLabel(d);tagEl.className='tag '+tagClass(d)}}
|
|
|
- else if(ev==='status'){contentDiv.innerHTML='<div class="thinking"><span class="think-dot"></span> '+d+'</div>'}
|
|
|
- else if(ev==='meta'){try{var meta=JSON.parse(d);if(meta.cid){CID=meta.cid;loadHistory()}if(meta.intent){var t=aiDiv.querySelector('.tag');if(t){t.textContent=intentLabel(meta.intent);t.className='tag '+tagClass(meta.intent)}}if(meta.sources&&meta.sources.length){var srcDiv=document.createElement('div');srcDiv.className='source';var parts=[];for(var j=0;j<Math.min(5,meta.sources.length);j++){var s=meta.sources[j];parts.push('<span>'+(s.section||s.name||'')+' ('+Number(s.score||0).toFixed(2)+')</span>')}srcDiv.innerHTML='📚 '+parts.join(' · ');aiDiv.appendChild(srcDiv)}}catch(e){}}
|
|
|
- else{out+=d;contentDiv.innerHTML=md2html(out)+'<span class="cursor">|</span>'}ev='';document.getElementById('chat').scrollTop=document.getElementById('chat').scrollHeight}}
|
|
|
- aiDiv.classList.remove('streaming');contentDiv.innerHTML=md2html(out);contentDiv.querySelector('.cursor')?.remove();
|
|
|
- }catch(e){if(e.name!=='AbortError'){contentDiv.innerHTML='<span style="color:var(--rd)">❌ '+e.message+'</span>';aiDiv.classList.remove('streaming')}}
|
|
|
- ABORT=null;STREAMING=false;
|
|
|
- if(MQ.length>0){setTimeout(processQ,300);return}
|
|
|
- document.getElementById('queue').style.display='none';
|
|
|
- document.getElementById('stopBtn').style.display='none';
|
|
|
+ var d=await r.json();
|
|
|
+ var intent=d.intent||'';
|
|
|
+ var answer=d.answer||'无结果';
|
|
|
+ var sources=d.sources||[];
|
|
|
+ var cid=d.conversation_id||'';
|
|
|
+ if(cid){CID=cid;loadHistory()}
|
|
|
+
|
|
|
+ // update tag
|
|
|
+ if(intent){var tagEl=aiDiv.querySelector('.tag');if(tagEl){tagEl.textContent=intentLabel(intent);tagEl.className='tag '+tagClass(intent)}}
|
|
|
+
|
|
|
+ // show answer with typing effect
|
|
|
+ var idx=0;
|
|
|
+ var timer=setInterval(function(){
|
|
|
+ if(idx>=answer.length){clearInterval(timer);
|
|
|
+ contentDiv.innerHTML=md2html(answer);contentDiv.querySelector('.cursor')?.remove();
|
|
|
+ aiDiv.classList.remove('streaming');
|
|
|
+ // add sources
|
|
|
+ if(sources.length){
|
|
|
+ var srcDiv=document.createElement('div');srcDiv.className='source';var parts=[];
|
|
|
+ for(var j=0;j<Math.min(5,sources.length);j++){var s=sources[j];parts.push('<span>'+(s.section||s.name||'')+' ('+Number(s.score||0).toFixed(2)+')</span>')}
|
|
|
+ srcDiv.innerHTML='📚 '+parts.join(' · ');aiDiv.appendChild(srcDiv)}
|
|
|
+ STREAMING=false;document.getElementById('stopBtn').style.display='none';
|
|
|
+ document.getElementById('queue').style.display='none';
|
|
|
+ document.getElementById('chat').scrollTop=document.getElementById('chat').scrollHeight;
|
|
|
+ if(MQ.length>0)setTimeout(processQ,300);return}
|
|
|
+ idx+=3;
|
|
|
+ contentDiv.innerHTML=md2html(answer.substring(0,idx))+'<span class="cursor">|</span>';
|
|
|
+ document.getElementById('chat').scrollTop=document.getElementById('chat').scrollHeight;
|
|
|
+ },15);
|
|
|
+ }catch(e){
|
|
|
+ if(e.name!=='AbortError'){contentDiv.innerHTML='<span style="color:var(--rd)">❌ '+e.message+'</span>';aiDiv.classList.remove('streaming')}
|
|
|
+ STREAMING=false;document.getElementById('stopBtn').style.display='none';
|
|
|
+ }
|
|
|
+ ABORT=null;
|
|
|
}
|
|
|
|
|
|
window.feedback=async function(msgId,type,btn){
|