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",
|
||||
"version": "2.9.0",
|
||||
"version": "2.9.6",
|
||||
"description": "Local AI agent framework powered by Ollama - OpenClaw alternative",
|
||||
"main": "dist/index.js",
|
||||
"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';
|
||||
function pptxProjectSlug(raw: string) {
|
||||
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 {
|
||||
if (fs.statSync(abs).isDirectory()) { scan(abs, relF); 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);
|
||||
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()) {
|
||||
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 });
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: String(err) });
|
||||
|
||||
@@ -408,6 +408,13 @@ export const pdfExtractImagesTool = {
|
||||
|
||||
// --- figures mode: render pages then auto-crop figures/tables with OpenCV ---
|
||||
if (mode === 'figures' || mode === 'both') {
|
||||
// Skip expensive re-extraction if figure files already exist in outDir
|
||||
const existingFigs = fs.existsSync(outDir)
|
||||
? fs.readdirSync(outDir).filter(f => f.startsWith('figure_') && f.endsWith('.png'))
|
||||
: [];
|
||||
if (existingFigs.length > 0 && !pageFrom && !pageTo) {
|
||||
extracted.push(...existingFigs.sort().map(f => `${outDirRel}/${f}`));
|
||||
} else {
|
||||
const figDpi = Math.min(300, Math.max(100, dpi));
|
||||
const ppmArgs = ['-png', '-r', String(figDpi)];
|
||||
if (pageFrom) ppmArgs.push('-f', String(pageFrom));
|
||||
@@ -425,6 +432,7 @@ export const pdfExtractImagesTool = {
|
||||
}
|
||||
extracted.push(...figFiles.map(f => `${outDirRel}/${f}`));
|
||||
}
|
||||
}
|
||||
|
||||
if (extracted.length === 0) {
|
||||
const errMsg = errors.length ? errors.join('; ') : 'No images found in the PDF';
|
||||
|
||||
+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