Release 2.9.5: PPTX wizard improvements + PDF extraction fixes
- PDF: skip references/bibliography section from text extraction
- PDF: add minimal system prompt for translate sessions (translation-only, no tool calls)
- Wizard: image download button per card + bulk download all
- Wizard: translation modal download button (saves as _ko.txt / _en.txt)
- Wizard: save papers.json immediately on upload, after text extraction, and before outline step
- Wizard: short image directory names (22 chars + timestamp suffix)
- Wizard: project-images scans workspace root dir alongside pptx/ folder
- Server: pptx/list scans only pptx/ folder (workspace root scanning removed after file migration)
- create_presentation: save output to workspace/pptx/{slug}/ instead of workspace root
- edit_presentation: fuzzy path search includes pptx/ subfolder
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+51
-3
@@ -344,6 +344,7 @@ body{background:var(--bg);color:var(--text);font-family:'Segoe UI',system-ui,san
|
||||
<div style="display:flex;align-items:center;gap:8px;margin-bottom:6px">
|
||||
<span style="font-size:10px;color:var(--muted);font-weight:600;text-transform:uppercase;letter-spacing:.04em">캡처 이미지 <span id="imgcnt"></span></span>
|
||||
<button id="btn-analyze-imgs" onclick="analyzeImages()" style="font-size:11px;color:#10b981;background:rgba(16,185,129,.1);border:1px solid #10b981;border-radius:5px;padding:2px 8px;cursor:pointer;flex-shrink:0" title="미분석 이미지를 mistral vision으로 재분석 (준비 실행 시 자동 처리됨)">🔍 재분석</button>
|
||||
<button onclick="downloadAllImgs()" style="font-size:11px;color:#60a5fa;background:rgba(96,165,250,.1);border:1px solid #60a5fa;border-radius:5px;padding:2px 8px;cursor:pointer;flex-shrink:0" title="전체 이미지 다운로드">⬇ 전체</button>
|
||||
<label style="display:flex;align-items:center;gap:4px;font-size:11px;color:var(--brand2);background:rgba(99,102,241,.1);border:1px solid var(--brand);border-radius:5px;padding:2px 8px;cursor:pointer;flex-shrink:0" title="이미지 파일 업로드">
|
||||
<input type="file" id="img-upload-input" accept="image/*" multiple style="display:none" onchange="handleImgUpload(this.files)">
|
||||
+ 이미지 업로드
|
||||
@@ -532,6 +533,7 @@ body{background:var(--bg);color:var(--text);font-family:'Segoe UI',system-ui,san
|
||||
<button class="btn bg" style="padding:3px 10px;font-size:11px" id="txl-tab-en" onclick="switchTxlTab('en')">원문</button>
|
||||
<button class="btn bg" style="padding:3px 10px;font-size:11px" id="txl-tab-ko" onclick="switchTxlTab('ko')">번역</button>
|
||||
<button class="btn bg" style="padding:3px 10px;font-size:11px" onclick="copyTxlText()">복사</button>
|
||||
<button class="btn bg" style="padding:3px 10px;font-size:11px" onclick="downloadTxlText()">⬇ 다운로드</button>
|
||||
<button class="btn bg" style="padding:3px 10px;font-size:11px" onclick="closeTxlModal()">✕</button>
|
||||
</div>
|
||||
<div class="txlmodal-body" id="txl-body"></div>
|
||||
@@ -878,8 +880,13 @@ async function deleteSelectedProject(){
|
||||
const r=await fetch('/api/pptx/delete-project',{...jh(),method:'DELETE',body:JSON.stringify({project})});
|
||||
const d=await r.json();
|
||||
if(!d.success) throw new Error(d.error||'삭제 실패');
|
||||
papers=[]; selected=new Set(); paperMeta={};
|
||||
paperTexts={}; paperTranslations={}; txlStatus={};
|
||||
prepLog={}; extractedImages=[]; imageCaptions={}; abPaper=null;
|
||||
outline=null; outlineEdits={};
|
||||
await loadProjList();
|
||||
sel.value='';
|
||||
goStep(1);
|
||||
alert(`"${project}" 삭제 완료.`);
|
||||
}catch(e){ alert('삭제 오류: '+e.message); }
|
||||
}
|
||||
@@ -1519,7 +1526,7 @@ function updOutlineBtn() {
|
||||
btn=document.createElement('button');
|
||||
btn.id='s2-outline-btn'; btn.className='btn bp';
|
||||
btn.textContent='✨ 아웃라인 생성 →';
|
||||
btn.onclick=()=>{ goStep(3); generateOutline(); };
|
||||
btn.onclick=()=>{ savePapersToProject(); goStep(3); generateOutline(); };
|
||||
document.querySelector('#step-2 .bbar').appendChild(btn);
|
||||
}
|
||||
} else {
|
||||
@@ -1564,6 +1571,19 @@ function copyTxlText(){
|
||||
const s=document.getElementById('txl-status'); s.textContent='클립보드에 복사됨'; setTimeout(()=>s.textContent='',2000);
|
||||
});
|
||||
}
|
||||
function downloadTxlText(){
|
||||
const t=document.getElementById('txl-body').textContent;
|
||||
if(!t.trim()) return;
|
||||
const label=txlViewKey?paperLabel(txlViewKey):'translation';
|
||||
const suffix=txlTabMode==='ko'?'_ko':'_en';
|
||||
const filename=(label+suffix).replace(/[/\\?%*:|"<>]/g,'_').slice(0,80)+'.txt';
|
||||
const blob=new Blob([t],{type:'text/plain;charset=utf-8'});
|
||||
const a=document.createElement('a');
|
||||
a.href=URL.createObjectURL(blob);
|
||||
a.download=filename;
|
||||
document.body.appendChild(a); a.click(); document.body.removeChild(a);
|
||||
setTimeout(()=>URL.revokeObjectURL(a.href),1000);
|
||||
}
|
||||
|
||||
// ── PDF 직접 업로드 ──────────────────────────────────────────────────────────
|
||||
function handlePdfDrop(e) {
|
||||
@@ -1597,6 +1617,7 @@ async function uploadPdfs(files) {
|
||||
}
|
||||
dz.innerHTML='📄 PDF 파일을 드래그하거나 클릭해서 선택<br><span style="font-size:10px">여러 파일 동시 업로드 가능</span>';
|
||||
renderUploadList(); updSelUI();
|
||||
savePapersToProject();
|
||||
}
|
||||
function removeUpload(idx){
|
||||
const p=papers[idx]; if(!p||!p._isUpload)return;
|
||||
@@ -2060,6 +2081,8 @@ async function startPrep() {
|
||||
prepLog[key].txt='skip';
|
||||
}
|
||||
updProg(key); updOverall();
|
||||
// 텍스트 추출 완료 즉시 저장 (번역 전에도 경로 보존)
|
||||
if(prepLog[key].txt==='done') savePapersToProject();
|
||||
// 텍스트 추출 성공 + 자동 번역 ON이면 백그라운드 번역 시작
|
||||
if(autoTranslate && prepLog[key].txt==='done' && paperTexts[key]) translatePdfText(key,paperTexts[key]);
|
||||
|
||||
@@ -2103,9 +2126,10 @@ function renderImgGrid(){
|
||||
const cap=imageCaptions[img.url]||'';
|
||||
const excluded=cap==='제외';
|
||||
const capHtml=cap?`<div style="font-size:8px;padding:1px 4px;line-height:1.3;color:${excluded?'#f87171':'#34d399'};overflow:hidden;text-overflow:ellipsis;white-space:nowrap" title="${esc(cap)}">${excluded?'⛔ 제외':cap}</div>`:'';
|
||||
return `<div class="icard" title="${esc(img.label)}" style="position:relative;opacity:${excluded?.5:1}" onmouseenter="this.querySelector('.icard-del').style.opacity=1" onmouseleave="this.querySelector('.icard-del').style.opacity=0">
|
||||
return `<div class="icard" title="${esc(img.label)}" style="position:relative;opacity:${excluded?.5:1}" onmouseenter="this.querySelectorAll('.icard-hover').forEach(b=>b.style.opacity=1)" onmouseleave="this.querySelectorAll('.icard-hover').forEach(b=>b.style.opacity=0)">
|
||||
<img src="${esc(img.url)}" loading="lazy" onerror="this.style.display='none'" onclick="showImgPreview('${esc(img.url)}','${esc(img.label)}')" style="cursor:zoom-in;width:100%;height:100%;object-fit:cover">
|
||||
<button class="icard-del" onclick="event.stopPropagation();deleteImg(${i})" style="position:absolute;top:4px;right:4px;opacity:0;transition:.15s;background:rgba(220,38,38,.85);border:none;color:#fff;border-radius:4px;width:22px;height:22px;font-size:13px;cursor:pointer;display:flex;align-items:center;justify-content:center;line-height:1" title="삭제">✕</button>
|
||||
<button class="icard-hover icard-del" onclick="event.stopPropagation();deleteImg(${i})" style="position:absolute;top:4px;right:4px;opacity:0;transition:.15s;background:rgba(220,38,38,.85);border:none;color:#fff;border-radius:4px;width:22px;height:22px;font-size:13px;cursor:pointer;display:flex;align-items:center;justify-content:center;line-height:1" title="삭제">✕</button>
|
||||
<button class="icard-hover" onclick="event.stopPropagation();downloadImg(${i})" style="position:absolute;top:4px;left:4px;opacity:0;transition:.15s;background:rgba(37,99,235,.85);border:none;color:#fff;border-radius:4px;width:22px;height:22px;font-size:13px;cursor:pointer;display:flex;align-items:center;justify-content:center;line-height:1" title="다운로드">⬇</button>
|
||||
<div class="ilabel" style="overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:9px;padding:2px 5px">${esc(img.label)}</div>
|
||||
${capHtml}
|
||||
</div>`;
|
||||
@@ -2154,6 +2178,30 @@ async function deleteImg(i){
|
||||
renderImgGrid();
|
||||
}
|
||||
|
||||
async function downloadImg(i) {
|
||||
const img = extractedImages[i];
|
||||
const filename = decodeURIComponent(img.url.split('/').pop().split('?')[0]) || 'image.png';
|
||||
try {
|
||||
const r = await fetch(img.url, {credentials: 'include'});
|
||||
const blob = await r.blob();
|
||||
const a = document.createElement('a');
|
||||
a.href = URL.createObjectURL(blob);
|
||||
a.download = filename;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
setTimeout(() => URL.revokeObjectURL(a.href), 1000);
|
||||
} catch(e) { alert('다운로드 실패: ' + e.message); }
|
||||
}
|
||||
|
||||
async function downloadAllImgs() {
|
||||
if (!extractedImages.length) return;
|
||||
for (let i = 0; i < extractedImages.length; i++) {
|
||||
await downloadImg(i);
|
||||
if (i < extractedImages.length - 1) await new Promise(r => setTimeout(r, 400));
|
||||
}
|
||||
}
|
||||
|
||||
async function doImgSearch() {
|
||||
const q = document.getElementById('img-search-input').value.trim();
|
||||
const res = document.getElementById('img-search-results');
|
||||
|
||||
Reference in New Issue
Block a user