Release 2.9.4: PPTX wizard figure analysis + skill model trigger fix

- pptx-wizard: add "🔍 이미지 분석" button in Step 2 image panel
  * calls /api/pptx/analyze-image per figure via mistral vision
  * shows per-image progress (1/10, 2/10...)
  * "제외" caption → dimmed card + filtered from outline prompt
  * captions shown below each thumbnail in the grid
- server: POST /api/pptx/analyze-image endpoint
  * resolves /api/files/ path to workspace file, thumbnails if >300KB
  * calls orchestration.secondary vision model (mistral-large-3:675b-cloud)
  * caches results in project/.image-captions.json (skips on re-analysis)
- pptx-wizard: generateOutline() includes captions in imgList
  * "제외" images filtered out before sending to LLM
  * format: "/api/files/... (figure_p2_1) — 카플란-마이어 생존곡선 3그룹"
- skills-manager: fix getModelOverrideForUser() trigger filtering
  * was returning skill model for ALL messages regardless of trigger keywords
  * now only overrides when message matches skill triggers (same logic as buildPromptContext)
  * presenter model (mistral) now only applies for 발표/pptx/슬라이드 keywords

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
kim
2026-06-02 14:58:22 +09:00
co-authored by Claude Sonnet 4.6
parent 69734e2e58
commit edf2c39472
3 changed files with 131 additions and 11 deletions
+42 -7
View File
@@ -343,6 +343,7 @@ body{background:var(--bg);color:var(--text);font-family:'Segoe UI',system-ui,san
<div style="flex:2;min-width:0">
<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="AI(mistral vision)가 각 이미지 내용 분석 — 아웃라인 생성 시 정확한 슬라이드 배치에 활용">🔍 이미지 분석</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)">
+ 이미지 업로드
@@ -762,7 +763,7 @@ function updSrcState(){
const checks=[...document.querySelectorAll('.src-chk input')];
if(checks.every(c=>!c.checked)) checks[0].checked=true;
}
let extractedImages = [], paperTexts = {};
let extractedImages = [], paperTexts = {}, imageCaptions = {};
let prepLog = {}, outline = null, outlineEdits = {}, currentProcessingKey = null;
let pptxRelPath = null, previewImages = [];
let currentStep = 1, abTabMode = 'en', modalSlide = -1;
@@ -854,7 +855,7 @@ async function savePapersToProject() {
function newProject(){
papers=[]; selected=new Set(); paperMeta={};
paperTexts={}; paperTranslations={}; txlStatus={};
prepLog={}; extractedImages={}; abPaper=null;
prepLog={}; extractedImages={}; imageCaptions={}; abPaper=null;
outline=null; outlineEdits={}; pptxRelPath=null; previewImages=[];
document.querySelectorAll('.preview-card[data-edited]').forEach(c=>delete c.dataset.edited);
const sel=document.getElementById('cfg-open-proj'); if(sel) sel.value='';
@@ -907,7 +908,7 @@ async function onOpenProjChange(relPath){
document.querySelectorAll('.preview-card[data-edited]').forEach(c=>delete c.dataset.edited);
papers=[]; selected=new Set(); paperMeta={};
paperTexts={}; paperTranslations={}; txlStatus={};
prepLog={}; extractedImages=[]; abPaper=null;
prepLog={}; extractedImages=[]; imageCaptions={}; abPaper=null;
// PPTX 없는 프로젝트 — step 1로 이동해서 논문 목록 확인
if(!hasPptx){
@@ -2071,12 +2072,42 @@ function renderImgGrid(){
const g=document.getElementById('imgrid'); if(!g)return;
document.getElementById('imgcnt').textContent='('+extractedImages.length+')';
if(!extractedImages.length){g.innerHTML='<div style="color:var(--muted);font-size:11px">없음</div>';return;}
g.innerHTML=extractedImages.map((img,i)=>
`<div class="icard" title="${esc(img.label)}" style="position:relative" onmouseenter="this.querySelector('.icard-del').style.opacity=1" onmouseleave="this.querySelector('.icard-del').style.opacity=0">
g.innerHTML=extractedImages.map((img,i)=>{
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">
<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>
<div class="ilabel" style="overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:9px;padding:2px 5px">${esc(img.label)}</div>
</div>`).join('');
${capHtml}
</div>`;
}).join('');
}
async function analyzeImages(){
if(!extractedImages.length){setMsg(2,'분석할 이미지가 없습니다.','var(--yellow)');return;}
const btn=document.getElementById('btn-analyze-imgs');
btn.disabled=true;
const project=resolveProjectSlug();
const total=extractedImages.length;
let done=0;
for(const img of extractedImages){
btn.textContent=`🔍 ${done}/${total}`;
if(!imageCaptions[img.url]){
try{
const r=await fetch('/api/pptx/analyze-image',{...jh(),method:'POST',body:JSON.stringify({url:img.url,label:img.label,project})});
const d=await r.json();
if(d.caption) imageCaptions[img.url]=d.caption;
}catch(e){console.warn('[analyzeImages]',e);}
}
done++;
renderImgGrid();
}
btn.disabled=false;
btn.textContent='🔍 이미지 분석';
const excluded=Object.values(imageCaptions).filter(c=>c==='제외').length;
setMsg(2,`이미지 분석 완료 (${total}장${excluded?', 제외 '+excluded+'장':''})`, 'var(--green)');
}
async function deleteImg(i){
@@ -2194,7 +2225,11 @@ async function generateOutline() {
contentBlocks.push(block);
}
const imgList=extractedImages.slice(0,30).map((img,i)=>`${i+1}. ${img.url} (${img.label})`).join('\n');
const imgList=extractedImages
.filter(img=>(imageCaptions[img.url]||'')!=='제외')
.slice(0,30)
.map((img,i)=>{const cap=imageCaptions[img.url]?` — ${imageCaptions[img.url]}`:'';return `${i+1}. ${img.url} (${img.label})${cap}`;})
.join('\n');
const prompt=`[PPTX 워크플로 시작]\n요청: ${topic}\nPMCIDs: ${pmcids}\n템플릿: ${tmpl}${skin?'\n배경 스킨: '+skin:''}\n슬라이드 수: ${slides}장\n\n=== 논문 내용 ===\n${contentBlocks.join('\n\n---\n')}\n\n=== 추출된 이미지 ===\n${imgList||'없음'}\n\n위 논문 내용과 이미지를 활용해 슬라이드 아웃라인을 \`\`\`pptx-outline 블록으로 출력해주세요. create_presentation은 아직 호출하지 마세요.`;