모델 프로파일 기반 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

View File

@@ -87,6 +87,11 @@ var fg = TryFindResource("PrimaryText") as Brush ?? Brushes.Black;
- 선택형: 커스텀 Popup 드롭다운 (`[라벨: 현재값 ▾]`)
- AI/고급 설정 항목 옆에 `?` 도움말 아이콘 + 커스텀 다크 툴팁 (`HelpTooltipStyle`)
- 설정 저장 시 `CustomMessageBox`로 완료 알림
- 설정에서 `on/off` 또는 숫자 입력이 필요한 신규 항목은 **기존 양식에 맞춰 통일해서 추가**해야 함
- `on/off` 항목은 예외 없이 `ToggleSwitch` 스타일을 사용하고, 기본 CheckBox/임의 토글 버튼으로 새로 만들지 않음
- 숫자 입력 항목은 가능하면 **텍스트박스 직접 입력 대신 기존 슬라이더 + 현재값 배지 패턴**을 우선 사용하며, 범위가 명확한 값은 반드시 이 패턴을 기본으로 채택
- 숫자 설정을 부득이하게 텍스트 입력으로 받을 경우에도, 먼저 기존 설정창/AX Agent 내부 설정에 같은 유형의 컨트롤이 있는지 확인하고 그 양식을 재사용해야 함
- 동일 성격의 설정은 메인 설정과 AX Agent 내부 설정에서 **표현 방식이 서로 다르면 안 되며**, 기존에 쓰던 컨트롤러/레이아웃 기준으로 맞춰 추가
### AX Agent 표현 수준 (필수)
- AX Agent UI 표현 수준은 설정에서 반드시 3단계로 제공: **`풍부하게` / `적절하게` / `간단하게`**

View File

@@ -1473,3 +1473,13 @@ MIT License
- Cowork/Chat 하단의 프리셋 안내 카드가 실제 결과를 가리던 문제를 수정했습니다. 이제 대화에 사용자/assistant 메시지가 생기거나 실행 중일 때는 해당 카드가 자동으로 숨겨집니다.
- [ChatWindow.FooterPresentation.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.FooterPresentation.cs)에 남아 있던 깨진 한글 워터마크/안내 문구를 정상 한국어로 정리했습니다.
- 라이브 타이핑 속도를 조정해 SSE 및 Cowork/Code 최종 프리뷰가 한 번에 붙는 느낌을 줄이고, 더 눈에 보이게 점진적으로 출력되도록 보정했습니다.
- 업데이트: 2026-04-08 10:12 (KST)
- 등록 모델에 `동작 프로파일`을 추가했습니다. 이제 모델별로 `균형 / 도구 호출 우선 / 추론 우선 / 읽기 속도 우선 / 문서 생성 우선` 성향을 저장할 수 있고, 편집/추가 모두 내부 설정과 일반 설정에서 같은 값으로 유지됩니다.
- Cowork/Code 루프는 현재 활성 모델의 프로파일을 읽어 no-tool 감지 임계값, 도구 미호출 재시도, 문서 생성 재시도, terminal evidence gate, 읽기 도구 병렬 배치 수를 다르게 적용합니다.
- AX Agent 내부 설정의 Temperature 항목에 `자동 / 사용자 지정` 전환을 추가했습니다. 자동일 때는 등록 모델 프로파일의 temperature 정책을 따르고, 사용자 지정일 때만 슬라이더 값이 실제 tool 호출 온도로 적용됩니다.
- 업데이트: 2026-04-08 10:38 (KST)
- 모델 실행 프로파일을 Cowork/Code 후속 게이트까지 더 깊게 연결했습니다. 이제 프로파일별로 post-tool verification, 코드 품질 게이트, 문서 검증 게이트, diff/실행 증거 게이트, final report 게이트의 강도를 다르게 적용합니다.
- `document_heavy` 프로파일은 `document_plan` 이후 장기 재시도보다 fallback 산출물 생성 쪽으로 더 빨리 전환되도록 조정했습니다.
- OpenAI/vLLM tool calling 바디에 `parallel_tool_calls` 힌트를 추가해 읽기 도구 병렬 실행 성향이 모델 요청 바디에도 반영되도록 보강했습니다.
- Cowork/Code 진행 표시에는 `계획 / 도구 / 검증 / 압축 / 폴백 / 재시도` 같은 단계 메타를 더 직접적으로 붙여, 오래 걸릴 때도 현재 단계가 더 잘 읽히게 했습니다.
- [docs/AX_AGENT_REGRESSION_PROMPTS.md](/E:/AX%20Copilot%20-%20Codex/docs/AX_AGENT_REGRESSION_PROMPTS.md)를 전면 정리해 `tool_call_strict`, `fast_readonly`, `document_heavy`, `reasoning_first` 프로파일별 회귀 시나리오를 고정했습니다.

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'
);
})();

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

File diff suppressed because it is too large Load Diff

Binary file not shown.

Binary file not shown.

View File

@@ -1,19 +0,0 @@
{
"runtimeOptions": {
"tfm": "net8.0",
"includedFrameworks": [
{
"name": "Microsoft.NETCore.App",
"version": "8.0.25"
},
{
"name": "Microsoft.WindowsDesktop.App",
"version": "8.0.25"
}
],
"configProperties": {
"System.Reflection.Metadata.MetadataUpdater.IsSupported": false,
"CSWINRT_USE_WINDOWS_UI_XAML_PROJECTIONS": false
}
}
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Some files were not shown because too many files have changed in this diff Show More