모델 프로파일 기반 Cowork/Code 루프와 진행 UX 고도화 반영

- 등록 모델 실행 프로파일을 검증 게이트, 문서 fallback, post-tool verification까지 확장 적용

- Cowork/Code 진행 카드에 계획/도구/검증/압축/폴백/재시도 단계 메타를 추가해 대기 상태 가시성 강화

- OpenAI/vLLM tool 요청에 병렬 도구 호출 힌트를 추가하고 회귀 프롬프트 문서를 프로파일 기준으로 전면 정리

- 검증: dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify\\ -p:IntermediateOutputPath=obj\\verify\\ (경고 0 / 오류 0)
This commit is contained in:
2026-04-08 13:41:57 +09:00
parent b391dfdfb3
commit a2c952879d
552 changed files with 8094 additions and 13595 deletions

132
create_html_preview.js Normal file
View File

@@ -0,0 +1,132 @@
const JSZip = require('jszip');
const fs = require('fs');
const path = require('path');
// EMU to inches: 1 inch = 914400 EMU
// Slide: 9144000 x 5143500 EMU = 10" x 5.625"
const EMU = 914400;
const SLIDE_W = 9144000;
const SLIDE_H = 5143500;
const SCALE = 960 / 10; // 96px per inch (at 100%)
function emuToPx(emu) {
return (emu / EMU) * SCALE;
}
function hexToRgb(hex) {
if (!hex || hex.length < 6) return '#888888';
return '#' + hex.slice(-6);
}
async function renderSlideHtml(xml, slideNum) {
// Extract background color
const bgMatch = xml.match(/p:bg>.*?<a:srgbClr val="([A-Fa-f0-9]{6})"/s);
const bgColor = bgMatch ? '#' + bgMatch[1] : '#F5F7FA';
// Parse all shapes and text boxes
const shapes = [];
// Find all spTree child elements (sp = shape/textbox, pic = image)
const spPattern = /<p:sp>(.+?)<\/p:sp>/gs;
let spMatch;
while ((spMatch = spPattern.exec(xml)) !== null) {
const spXml = spMatch[1];
// Get position
const offMatch = spXml.match(/<a:off x="(-?\d+)" y="(-?\d+)"/);
const extMatch = spXml.match(/<a:ext cx="(\d+)" cy="(\d+)"/);
if (!offMatch || !extMatch) continue;
const x = emuToPx(parseInt(offMatch[1]));
const y = emuToPx(parseInt(offMatch[2]));
const w = emuToPx(parseInt(extMatch[1]));
const h = emuToPx(parseInt(extMatch[2]));
// Get fill color
const fillMatch = spXml.match(/p:spPr[\s\S]*?<a:srgbClr val="([A-Fa-f0-9]{6})"/);
const fillColor = fillMatch ? '#' + fillMatch[1] : null;
// Get text content
const texts = [];
const paraPattern = /<a:p>([\s\S]*?)<\/a:p>/g;
let paraMatch;
while ((paraMatch = paraPattern.exec(spXml)) !== null) {
const paraXml = paraMatch[1];
const textMatches = [...paraXml.matchAll(/<a:t[^>]*>([^<]*)<\/a:t>/g)];
const paraText = textMatches.map(m => m[1]).join('');
if (paraText.trim()) texts.push(paraText.trim());
}
// Get font size
const szMatch = spXml.match(/sz="(\d+)"/);
const fontSize = szMatch ? parseInt(szMatch[1]) / 100 : 12;
// Get text color
const txtColorMatch = spXml.match(/<a:t>[\s\S]*?<a:srgbClr val="([A-Fa-f0-9]{6})"/);
const txtColor = txtColorMatch ? '#' + txtColorMatch[1] : '#FFFFFF';
shapes.push({ x, y, w, h, fillColor, texts, fontSize, txtColor });
}
// Build HTML
let shapesHtml = '';
for (const s of shapes) {
const bgStyle = s.fillColor ? `background-color: ${s.fillColor};` : '';
const textContent = s.texts.join('<br>');
shapesHtml += `<div style="position:absolute;left:${s.x.toFixed(1)}px;top:${s.y.toFixed(1)}px;width:${s.w.toFixed(1)}px;height:${s.h.toFixed(1)}px;${bgStyle}overflow:hidden;box-sizing:border-box;padding:2px 4px;">
<div style="font-size:${Math.min(s.fontSize, 20)}px;color:${s.txtColor};font-family:Arial,sans-serif;overflow:hidden;">${textContent}</div>
</div>`;
}
return `<div style="position:relative;width:960px;height:540px;background-color:${bgColor};overflow:hidden;border:1px solid #333;flex-shrink:0;">
<div style="position:absolute;top:2px;right:4px;font-size:10px;color:rgba(128,128,128,0.5);z-index:999">${slideNum}</div>
${shapesHtml}
</div>`;
}
async function createPreview(pptxPath, outputPath) {
const data = fs.readFileSync(pptxPath);
const zip = await JSZip.loadAsync(data);
const slideFiles = Object.keys(zip.files)
.filter(f => f.match(/^ppt\/slides\/slide\d+\.xml$/))
.sort((a, b) => {
const na = parseInt(a.match(/slide(\d+)/)[1]);
const nb = parseInt(b.match(/slide(\d+)/)[1]);
return na - nb;
});
let slidesHtml = '';
for (const sf of slideFiles) {
const slideNum = parseInt(sf.match(/slide(\d+)/)[1]);
const xml = await zip.file(sf).async('string');
slidesHtml += await renderSlideHtml(xml, slideNum);
}
const html = `<!DOCTYPE html>
<html><head><meta charset="utf-8">
<title>${path.basename(pptxPath)}</title>
<style>
body { background: #1a1a1a; font-family: Arial; margin: 0; padding: 20px; }
h2 { color: #fff; font-size: 14px; margin-bottom: 10px; }
.slides { display: flex; flex-direction: column; gap: 16px; align-items: flex-start; }
</style>
</head><body>
<h2>${path.basename(pptxPath)}</h2>
<div class="slides">${slidesHtml}</div>
</body></html>`;
fs.writeFileSync(outputPath, html);
console.log(`Preview: ${outputPath} (${slideFiles.length} slides)`);
}
(async () => {
await createPreview(
'E:/test/삼성디스플레이 vs LG디스플레이 비교 분석 보고서_20260407_1958.pptx',
'C:/Users/admin/AppData/Local/Temp/pptx_gen/preview1.html'
);
await createPreview(
'E:/test/삼성디스플레이 사업 영역 및 제품 강점 분석 보고서_20260407_1956.pptx',
'C:/Users/admin/AppData/Local/Temp/pptx_gen/preview2.html'
);
})();