Release 2.9.6: PPTX wizard search + image extraction improvements
- Add Europe PMC search source (free API, life sciences, PMC full-text) - Fix Europe PMC field mapping: journalInfo.journal.title, firstPublicationDate - Add PMC-only filter button with live paper count - Move sort selector to search row; shrink year input (flex:none, no spinners) - Uniform source checkbox sizing with per-source accent colors - Exclude preview/ directory from image grid (project-images endpoint) - Numeric sort for project images (slide1→2→9→10→11) - Skip PDF figure re-extraction if figure_* files already exist (pdf-extract.ts) - Preserve img:done state across wizard prep re-runs (savedImgDone) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+97
-26
@@ -22,10 +22,21 @@ body.theme-warm{
|
||||
}
|
||||
.theme-btn{background:none;border:1.5px solid var(--line);border-radius:6px;padding:4px 10px;font-size:12px;color:var(--muted);cursor:pointer;transition:.15s;}
|
||||
.theme-btn:hover,.theme-btn.active{border-color:var(--brand);color:var(--brand2);background:rgba(99,102,241,.08);}
|
||||
.src-chk{display:flex;align-items:flex-start;gap:6px;font-size:12px;font-weight:600;color:var(--muted);cursor:pointer;background:var(--panel2);border:1.5px solid var(--line);border-radius:7px;padding:7px 12px;transition:.15s;user-select:none;max-width:220px;}
|
||||
.src-chk:has(input:checked){border-color:var(--brand);background:rgba(99,102,241,.1);color:var(--brand2);}
|
||||
.src-chk input{accent-color:var(--brand);width:13px;height:13px;cursor:pointer;margin-top:2px;flex-shrink:0;}
|
||||
.src-chk-desc{display:block;font-size:10px;font-weight:400;color:var(--muted);margin-top:3px;line-height:1.5;}
|
||||
.src-chk{display:flex;align-items:flex-start;gap:6px;font-size:12px;font-weight:600;color:var(--muted);cursor:pointer;border:1.5px solid var(--line);border-radius:8px;padding:8px 11px;transition:.15s;user-select:none;width:168px;flex-shrink:0;box-sizing:border-box;}
|
||||
.src-chk input{width:13px;height:13px;cursor:pointer;margin-top:2px;flex-shrink:0;}
|
||||
.src-chk-desc{display:block;font-size:10px;font-weight:400;color:var(--muted);margin-top:4px;line-height:1.5;}
|
||||
#src-pubmed{background:rgba(239,68,68,.07);}
|
||||
#src-pubmed:has(input:checked){background:rgba(239,68,68,.18);border-color:#f87171;color:#fca5a5;}
|
||||
#src-pubmed input{accent-color:#ef4444;}
|
||||
#src-openalex{background:rgba(34,197,94,.07);}
|
||||
#src-openalex:has(input:checked){background:rgba(34,197,94,.18);border-color:var(--green);color:#86efac;}
|
||||
#src-openalex input{accent-color:#22c55e;}
|
||||
#src-semantic{background:rgba(251,191,36,.07);}
|
||||
#src-semantic:has(input:checked){background:rgba(251,191,36,.18);border-color:#fbbf24;color:#fde68a;}
|
||||
#src-semantic input{accent-color:#f59e0b;}
|
||||
#src-europepmc{background:rgba(96,165,250,.07);}
|
||||
#src-europepmc:has(input:checked){background:rgba(96,165,250,.18);border-color:#60a5fa;color:#bfdbfe;}
|
||||
#src-europepmc input{accent-color:#3b82f6;}
|
||||
*{margin:0;padding:0;box-sizing:border-box;}
|
||||
body{background:var(--bg);color:var(--text);font-family:'Segoe UI',system-ui,sans-serif;height:100vh;display:flex;flex-direction:column;overflow:hidden;font-size:14px;}
|
||||
|
||||
@@ -56,7 +67,9 @@ body{background:var(--bg);color:var(--text);font-family:'Segoe UI',system-ui,san
|
||||
.srow{display:flex;gap:8px;margin-bottom:12px;}
|
||||
.srow input{flex:1;background:var(--panel2);border:1px solid var(--line);border-radius:var(--r);padding:8px 12px;font-size:13px;color:var(--text);outline:none;}
|
||||
.srow input:focus{border-color:var(--brand);}
|
||||
.srow .yin{width:80px;flex-shrink:0;}
|
||||
.srow .yin{width:52px;flex:none;padding:8px 4px;text-align:center;}
|
||||
.srow .yin::-webkit-inner-spin-button,.srow .yin::-webkit-outer-spin-button{-webkit-appearance:none;}
|
||||
.srow .yin{-moz-appearance:textfield;}
|
||||
|
||||
/* Paper cards */
|
||||
.pcard{background:var(--panel2);border:1px solid var(--line);border-radius:var(--r);padding:11px 13px;margin-bottom:8px;display:flex;gap:10px;align-items:flex-start;transition:.15s;}
|
||||
@@ -276,22 +289,31 @@ body{background:var(--bg);color:var(--text);font-family:'Segoe UI',system-ui,san
|
||||
<span style="font-size:11px;font-weight:700;color:var(--muted);margin-top:7px">검색 소스:</span>
|
||||
<label class="src-chk" id="src-pubmed">
|
||||
<input type="checkbox" checked onchange="updSrcState()">
|
||||
<span>🔬 PubMed<span class="src-chk-desc">미국 NLM 의학·생명과학 전문 DB. PMC 등재 논문은 전문 PDF 다운로드 가능.</span></span>
|
||||
<span>🔬 PubMed<span class="src-chk-desc">의학·생명과학 전문. PMC 등재 논문은 전문 PDF 다운로드 가능.</span></span>
|
||||
</label>
|
||||
<label class="src-chk" id="src-openalex">
|
||||
<input type="checkbox" onchange="updSrcState()">
|
||||
<span>🌐 OpenAlex<span class="src-chk-desc">전 분야 2억+ 논문. API 키 불필요. 인용수·OA 여부 제공. OA 논문은 PDF 다운로드 가능.</span></span>
|
||||
<span>🌐 OpenAlex<span class="src-chk-desc">전 분야 2억+ 논문. Nature·Science 포함. OA PDF 제공.</span></span>
|
||||
</label>
|
||||
<label class="src-chk" id="src-semantic">
|
||||
<input type="checkbox" onchange="updSrcState()">
|
||||
<span>🧠 Semantic Scholar<span class="src-chk-desc">AI·CS·학제간 연구에 강함. 인용 영향력 지수 및 OA PDF 링크 제공.</span></span>
|
||||
<span>🧠 Semantic<span class="src-chk-desc">AI·CS·학제간 연구에 강함. 인용 영향력 지수 제공.</span></span>
|
||||
</label>
|
||||
<label class="src-chk" id="src-europepmc">
|
||||
<input type="checkbox" onchange="updSrcState()">
|
||||
<span>🇪🇺 Europe PMC<span class="src-chk-desc">PubMed보다 넓은 생명과학 커버리지. PMC 전문 포함.</span></span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="srow">
|
||||
<input id="qi" placeholder="예: sinus lift augmentation" onkeydown="if(event.key==='Enter')doSearch()">
|
||||
<input class="yin" id="qy" type="number" placeholder="연도" min="1990" max="2030" value="2017">
|
||||
<span style="font-size:11px;color:var(--muted);white-space:nowrap">이후 (선택)</span>
|
||||
<button class="btn bp" onclick="doSearch()" id="sbtn">🔍 검색</button>
|
||||
<select id="cfg-sort" style="background:var(--panel2);border:1px solid var(--line);border-radius:var(--r);padding:7px 9px;font-size:13px;color:var(--text);flex-shrink:0">
|
||||
<option value="relevance">관련도순</option>
|
||||
<option value="date">최신순</option>
|
||||
</select>
|
||||
<button id="pmc-filter-btn" class="btn bg" onclick="togglePmcFilter()" title="PMC 전문 있는 논문만 표시 (PDF 다운로드 가능)" style="flex-shrink:0;font-size:13px;padding:7px 11px;opacity:.6">PMC <span style="font-size:10px">PDF</span></button>
|
||||
<button class="btn bp" onclick="doSearch()" id="sbtn" style="font-size:13px;padding:7px 14px">🔍 검색</button>
|
||||
</div>
|
||||
<div id="search-legend" style="display:none;padding:7px 12px;background:rgba(99,102,241,.07);border-radius:7px;margin-bottom:6px;font-size:11px;color:var(--muted);line-height:1.7">
|
||||
<span style="color:var(--green);font-weight:700">■ PMC등재</span> — PubMed Central에 전문(PDF) 있음. 체크박스로 선택해 PPTX에 활용 가능.<br>
|
||||
@@ -443,13 +465,6 @@ body{background:var(--bg);color:var(--text);font-family:'Segoe UI',system-ui,san
|
||||
<label class="flabel">배경 스킨 <span id="skin-name-lbl" style="font-weight:400;text-transform:none;margin-left:4px"></span></label>
|
||||
<div class="skin-grid" id="skin-grid"></div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="flabel">정렬</label>
|
||||
<select id="cfg-sort">
|
||||
<option value="relevance">관련도순</option>
|
||||
<option value="date">최신순</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="flabel">PDF 이미지 자동 추출</label>
|
||||
<label style="display:flex;align-items:flex-start;gap:8px;font-size:12px;color:var(--text);cursor:pointer;line-height:1.5">
|
||||
@@ -760,6 +775,20 @@ const SKINS = [
|
||||
|
||||
// ── state ───────────────────────────────────────────────────────────────────
|
||||
let papers = [], selected = new Set(), paperMeta = {};
|
||||
let pmcFilterOn = false;
|
||||
function togglePmcFilter(){
|
||||
pmcFilterOn=!pmcFilterOn;
|
||||
const btn=document.getElementById('pmc-filter-btn');
|
||||
if(btn){
|
||||
if(pmcFilterOn){
|
||||
btn.style.cssText='flex-shrink:0;font-size:13px;padding:7px 11px;background:rgba(34,197,94,.25);color:#4ade80;border:2px solid #4ade80;border-radius:var(--r);font-weight:700;cursor:pointer;white-space:nowrap;opacity:1;';
|
||||
} else {
|
||||
btn.className='btn bg';
|
||||
btn.style.cssText='flex-shrink:0;font-size:13px;padding:7px 11px;opacity:.6;';
|
||||
}
|
||||
}
|
||||
renderPapers();
|
||||
}
|
||||
function updSrcState(){
|
||||
// 최소 하나는 선택 유지
|
||||
const checks=[...document.querySelectorAll('.src-chk input')];
|
||||
@@ -1229,6 +1258,7 @@ async function doSearch() {
|
||||
const usePubmed=document.querySelector('#src-pubmed input')?.checked;
|
||||
const useOpenalex=document.querySelector('#src-openalex input')?.checked;
|
||||
const useSemantic=document.querySelector('#src-semantic input')?.checked;
|
||||
const useEuropePmc=document.querySelector('#src-europepmc input')?.checked;
|
||||
const btn=document.getElementById('sbtn');
|
||||
btn.disabled=true; btn.textContent='검색 중...'; setMsg(1,'검색 중...','var(--brand2)');
|
||||
document.getElementById('results').innerHTML='<div style="color:var(--muted);padding:14px;text-align:center;font-size:12px">검색 중...</div>';
|
||||
@@ -1255,6 +1285,14 @@ async function doSearch() {
|
||||
citations:p.citations, inf_citations:p.inf_citations, oa_pdf:p.oa_pdf, abstract:p.abstract||'',
|
||||
}))).catch(()=>[])
|
||||
);
|
||||
if(useEuropePmc) tasks.push(
|
||||
fetch('/api/europepmc/search?q='+encodeURIComponent(q)+'&max=15&sort='+sort+(yr?'&min_year='+yr:''),cr())
|
||||
.then(r=>r.json()).then(d=>(d.data?.results||[]).map(p=>({
|
||||
_source:'europepmc', title:p.title, authors:p.authors,
|
||||
year:p.year, journal:p.journal, doi:p.doi, pmcid:p.pmcid, pmid:p.pmid, pub_types:p.pub_types,
|
||||
citations:p.citations, is_oa:p.is_oa, oa_url:p.oa_url, abstract:p.abstract,
|
||||
}))).catch(()=>[])
|
||||
);
|
||||
|
||||
const results=await Promise.all(tasks);
|
||||
// 병합 + DOI 기준 중복 제거 (pubmed 우선)
|
||||
@@ -1270,7 +1308,7 @@ async function doSearch() {
|
||||
}
|
||||
papers=[...papers.filter(p=>p._isUpload), ...merged];
|
||||
renderPapers();
|
||||
setMsg(1,merged.length+'건 검색됨 ('+[usePubmed&&'PubMed',useOpenalex&&'OpenAlex',useSemantic&&'Semantic'].filter(Boolean).join('+')+')','var(--green)');
|
||||
setMsg(1,merged.length+'건 검색됨 ('+[usePubmed&&'PubMed',useOpenalex&&'OpenAlex',useSemantic&&'Semantic',useEuropePmc&&'EuropePMC'].filter(Boolean).join('+')+')','var(--green)');
|
||||
document.getElementById('search-legend').style.display='';
|
||||
} catch(e){ setMsg(1,'검색 실패: '+e.message,'var(--red)'); document.getElementById('results').innerHTML='<div style="color:var(--red);padding:12px;font-size:12px">'+esc(e.message)+'</div>'; }
|
||||
finally{ btn.disabled=false; btn.textContent='🔍 검색'; }
|
||||
@@ -1278,7 +1316,16 @@ async function doSearch() {
|
||||
|
||||
function renderPapers() {
|
||||
const el=document.getElementById('results');
|
||||
const searchPapers=papers.filter(p=>!p._isUpload);
|
||||
let searchPapers=papers.filter(p=>!p._isUpload);
|
||||
const pmcPapers=searchPapers.filter(p=>!!p.pmcid||(p._source!=='pubmed'&&!!(p.oa_url||p.oa_pdf)));
|
||||
if(pmcFilterOn) searchPapers=pmcPapers;
|
||||
const btn=document.getElementById('pmc-filter-btn');
|
||||
if(btn&&searchPapers.length){
|
||||
const cnt=pmcPapers.length;
|
||||
btn.innerHTML=pmcFilterOn
|
||||
?`PMC <span style="font-size:10px">PDF</span> <span style="font-size:11px;font-weight:800">${cnt}</span>`
|
||||
:`PMC <span style="font-size:10px">PDF</span> <span style="font-size:10px;opacity:.7">${cnt}</span>`;
|
||||
}
|
||||
if(!searchPapers.length){el.innerHTML='<div style="color:var(--muted);padding:12px;font-size:12px">결과 없음</div>';return;}
|
||||
el.innerHTML=searchPapers.map((p)=>{
|
||||
const idx=papers.indexOf(p);
|
||||
@@ -1290,7 +1337,8 @@ function renderPapers() {
|
||||
const key=paperKey(p); const isSel=key?selected.has(key):false;
|
||||
const canSelect=!!key;
|
||||
const srcBadge=src==='openalex'?`<span class="badge" style="background:rgba(34,197,94,.15);color:var(--green)">OpenAlex</span>`:
|
||||
src==='semantic'?`<span class="badge" style="background:rgba(251,191,36,.15);color:var(--yellow)">Semantic Scholar</span>`:'';
|
||||
src==='semantic'?`<span class="badge" style="background:rgba(251,191,36,.15);color:var(--yellow)">Semantic Scholar</span>`:
|
||||
src==='europepmc'?`<span class="badge" style="background:rgba(96,165,250,.15);color:#60a5fa">Europe PMC</span>`:'';
|
||||
const citeBadge=p.citations?`<span class="badge" style="background:rgba(148,163,184,.1);color:var(--muted)">인용 ${p.citations}</span>`:'';
|
||||
const oaBadge=hasOaPdf?`<span class="badge" style="background:rgba(34,197,94,.15);color:var(--green)" title="Open Access PDF 있음">OA</span>`
|
||||
:(!hasPdf?`<span class="badge" style="background:rgba(148,163,184,.1);color:var(--muted)" title="PDF 없음 — 초록으로만 사용">초록만</span>`:'');
|
||||
@@ -1301,7 +1349,7 @@ function renderPapers() {
|
||||
<div class="psub">${esc(p.authors||'')} · ${esc(p.journal||'')} ${esc(p.year||'')}</div>
|
||||
<div style="margin-top:3px;display:flex;flex-wrap:wrap;gap:3px">
|
||||
${srcBadge}
|
||||
${src==='pubmed'?(hp?`<span class="badge bpmc" title="PMC 전문 PDF 가능">${pmcid}</span>`:'<span class="badge bnopmc">PMC미등재</span>'):''}
|
||||
${(src==='pubmed'||src==='europepmc')?(hp?`<span class="badge bpmc" title="PMC 전문 PDF 가능">${pmcid}</span>`:(src==='pubmed'?'<span class="badge bnopmc">PMC미등재</span>':'')):''}
|
||||
${p.year?`<span class="badge byear">${p.year}</span>`:''}
|
||||
${p.pub_types?`<span class="badge btype">${p.pub_types.split(',')[0].trim()}</span>`:''}
|
||||
${oaBadge}${citeBadge}
|
||||
@@ -1930,9 +1978,12 @@ async function startPrep() {
|
||||
btn.disabled=true; btn.textContent='진행 중...';
|
||||
setMsg(2,'준비 중...','var(--brand2)');
|
||||
const keys=Array.from(selected);
|
||||
// prepLog 리셋 전에 기존 pdfPath 보존 (재실행 시 재다운로드 방지)
|
||||
const savedPdfPaths={};
|
||||
for(const k of keys){ if(prepLog[k]?.pdfPath) savedPdfPaths[k]=prepLog[k].pdfPath; }
|
||||
// prepLog 리셋 전에 기존 pdfPath·img 완료 상태 보존 (재실행 시 재다운로드/재추출 방지)
|
||||
const savedPdfPaths={}, savedImgDone={};
|
||||
for(const k of keys){
|
||||
if(prepLog[k]?.pdfPath) savedPdfPaths[k]=prepLog[k].pdfPath;
|
||||
if(prepLog[k]?.img==='done') savedImgDone[k]={imgCount:prepLog[k].imgCount};
|
||||
}
|
||||
prepLog={}; renderProgList(); extractedImages=[];
|
||||
document.getElementById('imgrid').innerHTML='<div style="color:var(--muted);font-size:11px">📷 이미지 캡처 버튼으로 추가하세요</div>';
|
||||
|
||||
@@ -1945,7 +1996,8 @@ async function startPrep() {
|
||||
const isUpload=key.startsWith('UPL_');
|
||||
const paper=papers.find(p=>paperKey(p)===key);
|
||||
currentProcessingKey=key;
|
||||
prepLog[key]={pdf:isUpload?'done':'running',img:autoExtractImages?'waiting':'skip',txt:extractText?'waiting':'skip'};
|
||||
const imgInit=savedImgDone[key]?'done':(autoExtractImages?'waiting':'skip');
|
||||
prepLog[key]={pdf:isUpload?'done':'running',img:imgInit,imgCount:savedImgDone[key]?.imgCount,txt:extractText?'waiting':'skip'};
|
||||
updProg(key); updOverall();
|
||||
|
||||
try{
|
||||
@@ -2023,8 +2075,8 @@ async function startPrep() {
|
||||
setMsg(2,key+' 이미지 추출...','var(--brand2)');
|
||||
}
|
||||
|
||||
// 이미지 자동 추출 + mistral vision 분석 (토글 ON일 때만)
|
||||
if(autoExtractImages && pdfPath){
|
||||
// 이미지 자동 추출 + mistral vision 분석 (토글 ON, 이전 실행에서 완료되지 않은 경우만)
|
||||
if(autoExtractImages && pdfPath && !savedImgDone[key]){
|
||||
prepLog[key].img='running'; updProg(key);
|
||||
setMsg(2,paperLabel(key)+' 이미지 추출...','var(--brand2)');
|
||||
try{
|
||||
@@ -2060,6 +2112,25 @@ async function startPrep() {
|
||||
prepLog[key].img='done'; prepLog[key].imgCount=added;
|
||||
}catch(e2){ prepLog[key].img='fail'; }
|
||||
updProg(key);
|
||||
} else if(savedImgDone[key]){
|
||||
// 이전 실행에서 이미 추출 완료 — 서버에서 기존 파일 목록만 다시 로드
|
||||
if(pdfPath){
|
||||
try{
|
||||
const exr=await fetch('/api/pptx/extract-images',{...jh(),method:'POST',body:JSON.stringify({pdf_path:pdfPath,project:prepProject})});
|
||||
const exd=await exr.json();
|
||||
if(exd.success&&exd.data?.files){
|
||||
const pname=key.startsWith('UPL_')?(pdfPath.split('/').pop()||'').replace(/\.pdf$/i,''):key;
|
||||
for(const f of exd.data.files){
|
||||
const url='/api/files/'+f;
|
||||
if(!extractedImages.find(img=>img.url===url))
|
||||
extractedImages.push({url,label:pname+': '+f.split('/').pop(),pdf:pdfPath});
|
||||
}
|
||||
renderImgGrid();
|
||||
document.getElementById('imgcnt').textContent='('+extractedImages.length+')';
|
||||
}
|
||||
}catch{}
|
||||
}
|
||||
prepLog[key].img='done'; updProg(key);
|
||||
} else {
|
||||
prepLog[key].img='skip'; updProg(key);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user