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:
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "smallclaw",
|
"name": "smallclaw",
|
||||||
"version": "2.9.0",
|
"version": "2.9.6",
|
||||||
"description": "Local AI agent framework powered by Ollama - OpenClaw alternative",
|
"description": "Local AI agent framework powered by Ollama - OpenClaw alternative",
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
"bin": {
|
"bin": {
|
||||||
|
|||||||
@@ -9576,6 +9576,43 @@ app.get('/api/semantic/search', requireGatewayAuth, async (req: express.Request,
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
app.get('/api/europepmc/search', requireGatewayAuth, async (req: express.Request, res: express.Response) => {
|
||||||
|
try {
|
||||||
|
const query = String(req.query.q || req.query.query || '').trim();
|
||||||
|
const maxResults = Math.min(Number(req.query.max || 20), 50);
|
||||||
|
const minYear = req.query.min_year ? Number(req.query.min_year) : undefined;
|
||||||
|
const sort = req.query.sort === 'date' ? 'P_PDATE_D desc' : 'RELEVANCE';
|
||||||
|
if (!query) { res.status(400).json({ error: 'q parameter required' }); return; }
|
||||||
|
|
||||||
|
let q = query;
|
||||||
|
if (minYear) q += ` AND PUB_YEAR:[${minYear} TO 2099]`;
|
||||||
|
|
||||||
|
const url = `https://www.ebi.ac.uk/europepmc/webservices/rest/search?query=${encodeURIComponent(q)}&format=json&resultType=core&pageSize=${maxResults}&sort=${encodeURIComponent(sort)}`;
|
||||||
|
const resp = await fetch(url, { signal: AbortSignal.timeout(15000) });
|
||||||
|
if (!resp.ok) { res.status(502).json({ error: `Europe PMC ${resp.status}` }); return; }
|
||||||
|
const data = await resp.json() as any;
|
||||||
|
|
||||||
|
const results = (data.resultList?.result || []).map((p: any) => ({
|
||||||
|
title: p.title?.replace(/<[^>]+>/g, '').trim() || '',
|
||||||
|
authors: p.authorString || '',
|
||||||
|
year: p.firstPublicationDate ? String(p.firstPublicationDate).slice(0, 4) : (p.journalInfo?.yearOfPublication ? String(p.journalInfo.yearOfPublication) : ''),
|
||||||
|
journal: p.journalInfo?.journal?.title || '',
|
||||||
|
doi: p.doi || null,
|
||||||
|
pmid: p.pmid ? String(p.pmid) : null,
|
||||||
|
pmcid: p.pmcid ? String(p.pmcid).replace(/^PMC/i, '') : null,
|
||||||
|
pub_types: p.pubTypeList?.pubType?.filter((t: string) => !/^(research|review)-article$/i.test(t)).join(', ') || '',
|
||||||
|
is_oa: p.isOpenAccess === 'Y',
|
||||||
|
oa_url: p.isOpenAccess === 'Y' && p.pmcid ? `https://europepmc.org/article/PMC/${p.pmcid}` : null,
|
||||||
|
abstract: p.abstractText?.replace(/<[^>]+>/g, '') || '',
|
||||||
|
citations: p.citedByCount || 0,
|
||||||
|
}));
|
||||||
|
|
||||||
|
res.json({ success: true, data: { results } });
|
||||||
|
} catch (err) {
|
||||||
|
res.status(500).json({ error: String(err) });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
const PPTX_BASE = 'pptx';
|
const PPTX_BASE = 'pptx';
|
||||||
function pptxProjectSlug(raw: string) {
|
function pptxProjectSlug(raw: string) {
|
||||||
return String(raw || '').trim().replace(/[^a-zA-Z0-9가-힣_\-]/g, '_').replace(/_+/g, '_').replace(/^_+|_+$/g, '').toLowerCase().slice(0, 60);
|
return String(raw || '').trim().replace(/[^a-zA-Z0-9가-힣_\-]/g, '_').replace(/_+/g, '_').replace(/^_+|_+$/g, '').toLowerCase().slice(0, 60);
|
||||||
@@ -10095,7 +10132,7 @@ app.get('/api/pptx/project-images', requireGatewayAuth, async (req: express.Requ
|
|||||||
try {
|
try {
|
||||||
if (fs.statSync(abs).isDirectory()) { scan(abs, relF); continue; }
|
if (fs.statSync(abs).isDirectory()) { scan(abs, relF); continue; }
|
||||||
} catch { continue; }
|
} catch { continue; }
|
||||||
if (IMG_EXT.test(f) && !seenPaths.has(abs)) {
|
if (IMG_EXT.test(f) && !seenPaths.has(abs) && !relF.includes('/preview/')) {
|
||||||
seenPaths.add(abs);
|
seenPaths.add(abs);
|
||||||
images.push({ path: relF, name: f });
|
images.push({ path: relF, name: f });
|
||||||
}
|
}
|
||||||
@@ -10109,6 +10146,10 @@ app.get('/api/pptx/project-images', requireGatewayAuth, async (req: express.Requ
|
|||||||
if (wsRootDir !== projectDir && fs.existsSync(wsRootDir) && fs.statSync(wsRootDir).isDirectory()) {
|
if (wsRootDir !== projectDir && fs.existsSync(wsRootDir) && fs.statSync(wsRootDir).isDirectory()) {
|
||||||
scan(wsRootDir, slugOnly);
|
scan(wsRootDir, slugOnly);
|
||||||
}
|
}
|
||||||
|
images.sort((a, b) => {
|
||||||
|
const numOf = (s: string) => { const m = s.match(/(\d+)/g); return m ? m.map(n => n.padStart(6, '0')).join('_') : s; };
|
||||||
|
return numOf(a.name).localeCompare(numOf(b.name));
|
||||||
|
});
|
||||||
res.json({ images });
|
res.json({ images });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
res.status(500).json({ error: String(err) });
|
res.status(500).json({ error: String(err) });
|
||||||
|
|||||||
+23
-15
@@ -408,22 +408,30 @@ export const pdfExtractImagesTool = {
|
|||||||
|
|
||||||
// --- figures mode: render pages then auto-crop figures/tables with OpenCV ---
|
// --- figures mode: render pages then auto-crop figures/tables with OpenCV ---
|
||||||
if (mode === 'figures' || mode === 'both') {
|
if (mode === 'figures' || mode === 'both') {
|
||||||
const figDpi = Math.min(300, Math.max(100, dpi));
|
// Skip expensive re-extraction if figure files already exist in outDir
|
||||||
const ppmArgs = ['-png', '-r', String(figDpi)];
|
const existingFigs = fs.existsSync(outDir)
|
||||||
if (pageFrom) ppmArgs.push('-f', String(pageFrom));
|
? fs.readdirSync(outDir).filter(f => f.startsWith('figure_') && f.endsWith('.png'))
|
||||||
if (pageTo) ppmArgs.push('-l', String(pageTo));
|
: [];
|
||||||
ppmArgs.push(resolved, path.join(outDir, 'page'));
|
if (existingFigs.length > 0 && !pageFrom && !pageTo) {
|
||||||
try {
|
extracted.push(...existingFigs.sort().map(f => `${outDirRel}/${f}`));
|
||||||
await execFileAsync('pdftoppm', ppmArgs, { timeout: 120_000 });
|
} else {
|
||||||
} catch (err: any) {
|
const figDpi = Math.min(300, Math.max(100, dpi));
|
||||||
errors.push(`pdftoppm: ${String(err.message || err).slice(0, 200)}`);
|
const ppmArgs = ['-png', '-r', String(figDpi)];
|
||||||
|
if (pageFrom) ppmArgs.push('-f', String(pageFrom));
|
||||||
|
if (pageTo) ppmArgs.push('-l', String(pageTo));
|
||||||
|
ppmArgs.push(resolved, path.join(outDir, 'page'));
|
||||||
|
try {
|
||||||
|
await execFileAsync('pdftoppm', ppmArgs, { timeout: 120_000 });
|
||||||
|
} catch (err: any) {
|
||||||
|
errors.push(`pdftoppm: ${String(err.message || err).slice(0, 200)}`);
|
||||||
|
}
|
||||||
|
const { files: figFiles, errors: figErrors } = await extractFigures(outDir, outDir);
|
||||||
|
errors.push(...figErrors);
|
||||||
|
for (const f of fs.readdirSync(outDir).filter(f => f.startsWith('page-') && f.endsWith('.png'))) {
|
||||||
|
try { fs.unlinkSync(path.join(outDir, f)); } catch {}
|
||||||
|
}
|
||||||
|
extracted.push(...figFiles.map(f => `${outDirRel}/${f}`));
|
||||||
}
|
}
|
||||||
const { files: figFiles, errors: figErrors } = await extractFigures(outDir, outDir);
|
|
||||||
errors.push(...figErrors);
|
|
||||||
for (const f of fs.readdirSync(outDir).filter(f => f.startsWith('page-') && f.endsWith('.png'))) {
|
|
||||||
try { fs.unlinkSync(path.join(outDir, f)); } catch {}
|
|
||||||
}
|
|
||||||
extracted.push(...figFiles.map(f => `${outDirRel}/${f}`));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (extracted.length === 0) {
|
if (extracted.length === 0) {
|
||||||
|
|||||||
+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{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);}
|
.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{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:has(input:checked){border-color:var(--brand);background:rgba(99,102,241,.1);color:var(--brand2);}
|
.src-chk input{width:13px;height:13px;cursor:pointer;margin-top:2px;flex-shrink:0;}
|
||||||
.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:4px;line-height:1.5;}
|
||||||
.src-chk-desc{display:block;font-size:10px;font-weight:400;color:var(--muted);margin-top:3px;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;}
|
*{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;}
|
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{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{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 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 */
|
/* 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;}
|
.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>
|
<span style="font-size:11px;font-weight:700;color:var(--muted);margin-top:7px">검색 소스:</span>
|
||||||
<label class="src-chk" id="src-pubmed">
|
<label class="src-chk" id="src-pubmed">
|
||||||
<input type="checkbox" checked onchange="updSrcState()">
|
<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>
|
||||||
<label class="src-chk" id="src-openalex">
|
<label class="src-chk" id="src-openalex">
|
||||||
<input type="checkbox" onchange="updSrcState()">
|
<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>
|
||||||
<label class="src-chk" id="src-semantic">
|
<label class="src-chk" id="src-semantic">
|
||||||
<input type="checkbox" onchange="updSrcState()">
|
<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>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="srow">
|
<div class="srow">
|
||||||
<input id="qi" placeholder="예: sinus lift augmentation" onkeydown="if(event.key==='Enter')doSearch()">
|
<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">
|
<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>
|
<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>
|
||||||
<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">
|
<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>
|
<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>
|
<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 class="skin-grid" id="skin-grid"></div>
|
||||||
</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">
|
<div class="field">
|
||||||
<label class="flabel">PDF 이미지 자동 추출</label>
|
<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">
|
<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 ───────────────────────────────────────────────────────────────────
|
// ── state ───────────────────────────────────────────────────────────────────
|
||||||
let papers = [], selected = new Set(), paperMeta = {};
|
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(){
|
function updSrcState(){
|
||||||
// 최소 하나는 선택 유지
|
// 최소 하나는 선택 유지
|
||||||
const checks=[...document.querySelectorAll('.src-chk input')];
|
const checks=[...document.querySelectorAll('.src-chk input')];
|
||||||
@@ -1229,6 +1258,7 @@ async function doSearch() {
|
|||||||
const usePubmed=document.querySelector('#src-pubmed input')?.checked;
|
const usePubmed=document.querySelector('#src-pubmed input')?.checked;
|
||||||
const useOpenalex=document.querySelector('#src-openalex input')?.checked;
|
const useOpenalex=document.querySelector('#src-openalex input')?.checked;
|
||||||
const useSemantic=document.querySelector('#src-semantic input')?.checked;
|
const useSemantic=document.querySelector('#src-semantic input')?.checked;
|
||||||
|
const useEuropePmc=document.querySelector('#src-europepmc input')?.checked;
|
||||||
const btn=document.getElementById('sbtn');
|
const btn=document.getElementById('sbtn');
|
||||||
btn.disabled=true; btn.textContent='검색 중...'; setMsg(1,'검색 중...','var(--brand2)');
|
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>';
|
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||'',
|
citations:p.citations, inf_citations:p.inf_citations, oa_pdf:p.oa_pdf, abstract:p.abstract||'',
|
||||||
}))).catch(()=>[])
|
}))).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);
|
const results=await Promise.all(tasks);
|
||||||
// 병합 + DOI 기준 중복 제거 (pubmed 우선)
|
// 병합 + DOI 기준 중복 제거 (pubmed 우선)
|
||||||
@@ -1270,7 +1308,7 @@ async function doSearch() {
|
|||||||
}
|
}
|
||||||
papers=[...papers.filter(p=>p._isUpload), ...merged];
|
papers=[...papers.filter(p=>p._isUpload), ...merged];
|
||||||
renderPapers();
|
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='';
|
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>'; }
|
} 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='🔍 검색'; }
|
finally{ btn.disabled=false; btn.textContent='🔍 검색'; }
|
||||||
@@ -1278,7 +1316,16 @@ async function doSearch() {
|
|||||||
|
|
||||||
function renderPapers() {
|
function renderPapers() {
|
||||||
const el=document.getElementById('results');
|
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;}
|
if(!searchPapers.length){el.innerHTML='<div style="color:var(--muted);padding:12px;font-size:12px">결과 없음</div>';return;}
|
||||||
el.innerHTML=searchPapers.map((p)=>{
|
el.innerHTML=searchPapers.map((p)=>{
|
||||||
const idx=papers.indexOf(p);
|
const idx=papers.indexOf(p);
|
||||||
@@ -1290,7 +1337,8 @@ function renderPapers() {
|
|||||||
const key=paperKey(p); const isSel=key?selected.has(key):false;
|
const key=paperKey(p); const isSel=key?selected.has(key):false;
|
||||||
const canSelect=!!key;
|
const canSelect=!!key;
|
||||||
const srcBadge=src==='openalex'?`<span class="badge" style="background:rgba(34,197,94,.15);color:var(--green)">OpenAlex</span>`:
|
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 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>`
|
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>`:'');
|
:(!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 class="psub">${esc(p.authors||'')} · ${esc(p.journal||'')} ${esc(p.year||'')}</div>
|
||||||
<div style="margin-top:3px;display:flex;flex-wrap:wrap;gap:3px">
|
<div style="margin-top:3px;display:flex;flex-wrap:wrap;gap:3px">
|
||||||
${srcBadge}
|
${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.year?`<span class="badge byear">${p.year}</span>`:''}
|
||||||
${p.pub_types?`<span class="badge btype">${p.pub_types.split(',')[0].trim()}</span>`:''}
|
${p.pub_types?`<span class="badge btype">${p.pub_types.split(',')[0].trim()}</span>`:''}
|
||||||
${oaBadge}${citeBadge}
|
${oaBadge}${citeBadge}
|
||||||
@@ -1930,9 +1978,12 @@ async function startPrep() {
|
|||||||
btn.disabled=true; btn.textContent='진행 중...';
|
btn.disabled=true; btn.textContent='진행 중...';
|
||||||
setMsg(2,'준비 중...','var(--brand2)');
|
setMsg(2,'준비 중...','var(--brand2)');
|
||||||
const keys=Array.from(selected);
|
const keys=Array.from(selected);
|
||||||
// prepLog 리셋 전에 기존 pdfPath 보존 (재실행 시 재다운로드 방지)
|
// prepLog 리셋 전에 기존 pdfPath·img 완료 상태 보존 (재실행 시 재다운로드/재추출 방지)
|
||||||
const savedPdfPaths={};
|
const savedPdfPaths={}, savedImgDone={};
|
||||||
for(const k of keys){ if(prepLog[k]?.pdfPath) savedPdfPaths[k]=prepLog[k].pdfPath; }
|
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=[];
|
prepLog={}; renderProgList(); extractedImages=[];
|
||||||
document.getElementById('imgrid').innerHTML='<div style="color:var(--muted);font-size:11px">📷 이미지 캡처 버튼으로 추가하세요</div>';
|
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 isUpload=key.startsWith('UPL_');
|
||||||
const paper=papers.find(p=>paperKey(p)===key);
|
const paper=papers.find(p=>paperKey(p)===key);
|
||||||
currentProcessingKey=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();
|
updProg(key); updOverall();
|
||||||
|
|
||||||
try{
|
try{
|
||||||
@@ -2023,8 +2075,8 @@ async function startPrep() {
|
|||||||
setMsg(2,key+' 이미지 추출...','var(--brand2)');
|
setMsg(2,key+' 이미지 추출...','var(--brand2)');
|
||||||
}
|
}
|
||||||
|
|
||||||
// 이미지 자동 추출 + mistral vision 분석 (토글 ON일 때만)
|
// 이미지 자동 추출 + mistral vision 분석 (토글 ON, 이전 실행에서 완료되지 않은 경우만)
|
||||||
if(autoExtractImages && pdfPath){
|
if(autoExtractImages && pdfPath && !savedImgDone[key]){
|
||||||
prepLog[key].img='running'; updProg(key);
|
prepLog[key].img='running'; updProg(key);
|
||||||
setMsg(2,paperLabel(key)+' 이미지 추출...','var(--brand2)');
|
setMsg(2,paperLabel(key)+' 이미지 추출...','var(--brand2)');
|
||||||
try{
|
try{
|
||||||
@@ -2060,6 +2112,25 @@ async function startPrep() {
|
|||||||
prepLog[key].img='done'; prepLog[key].imgCount=added;
|
prepLog[key].img='done'; prepLog[key].imgCount=added;
|
||||||
}catch(e2){ prepLog[key].img='fail'; }
|
}catch(e2){ prepLog[key].img='fail'; }
|
||||||
updProg(key);
|
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 {
|
} else {
|
||||||
prepLog[key].img='skip'; updProg(key);
|
prepLog[key].img='skip'; updProg(key);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user