Mirror of design_agent_front/design-agent/ for shipping alongside backend.
Vite plugin (vitePluginPhaseZApi) endpoints :
- POST /api/run — spawn `python -m src.phase_z2_pipeline` with overrides
- GET /api/sample-mdx?mdx=03/04/05 — fixed sample MDX
- GET /frame-preview/{n} — figma preview thumbnails
- GET /data/runs/{run_id}/{path} — pipeline artifacts (final.html, step*.json, ...)
Env toggle forward (보고용) :
PHASE_Z_ALLOW_RESTRUCTURE / PHASE_Z_ALLOW_REJECT / PHASE_Z_MAX_RANK=32
Components :
- LeftMdxPanel (03/04/05 fix list + section tree)
- SlideCanvas (iframe + slideOverrideCss prop for inline CSS inject)
- FramePanel (label priority + confidence sort)
- LayoutPanel
README with mermaid diagrams covering the 5-step demo flow.
node_modules / dist / .manus-logs / .env excluded via .gitignore.
177 lines
5.3 KiB
TypeScript
177 lines
5.3 KiB
TypeScript
import type { UserSelection, SlidePlan, Zone, InternalRegion, LayoutPresetId } from "../types/designAgent";
|
|
|
|
/**
|
|
* Phase Z 초기 선택 상태 생성
|
|
* SlidePlan의 결과를 초기 값으로 사용 (Step 11까지의 결과 반영)
|
|
*/
|
|
export function createInitialUserSelection(slidePlan?: SlidePlan | null): UserSelection {
|
|
const initialSections: Record<string, string[]> = {};
|
|
const initialFrames: Record<string, string> = {};
|
|
|
|
if (slidePlan) {
|
|
slidePlan.zones.forEach(zone => {
|
|
// 1. 모든 섹션을 각자의 지정된 존에 할당 (초안 배치)
|
|
initialSections[zone.zone_id] = [...zone.section_ids];
|
|
|
|
// 2. 각 리전의 기본 frame.
|
|
// 2026-05-14 — backend frame_match_strategy.frame_id 가 있을 때만 init.
|
|
// null 인 경우 (backend current_default_candidate=None 등) frame_candidates[0]
|
|
// 로 자동 채우지 않음 → SlideCanvas 의 preview overlay 트리거 조건
|
|
// (override !== default) 안 발동. 사용자가 직접 frame 클릭해야 preview 보임.
|
|
// 배경 : 04-1 같은 case 에서 backend selection_path=rank_1 (env toggle 통과)
|
|
// 이어도 current_default=None 이면 default 와 override mismatch 로 preview 강제 발동.
|
|
zone.internal_regions.forEach(region => {
|
|
const topFrameId = region.frame_match_strategy.frame_id;
|
|
if (topFrameId) {
|
|
initialFrames[region.id] = topFrameId;
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
return {
|
|
selectedSectionId: null,
|
|
selectedZoneId: slidePlan?.zones[0]?.id || null,
|
|
selectedRegionId: slidePlan?.zones[0]?.internal_regions[0]?.id || null,
|
|
overrides: {
|
|
layout_preset: slidePlan?.layout_preset,
|
|
zone_frames: initialFrames,
|
|
zone_sections: initialSections,
|
|
zone_sizes: {},
|
|
zone_geometries: {},
|
|
},
|
|
};
|
|
}
|
|
|
|
export function saveZoneGeometry(
|
|
selection: UserSelection,
|
|
zoneId: string,
|
|
geometry: { x: number; y: number; w: number; h: number }
|
|
): UserSelection {
|
|
return {
|
|
...selection,
|
|
overrides: {
|
|
...selection.overrides,
|
|
zone_geometries: {
|
|
...selection.overrides.zone_geometries,
|
|
[zoneId]: geometry,
|
|
}
|
|
}
|
|
};
|
|
}
|
|
|
|
export function saveZoneSizes(selection: UserSelection, groupId: string, sizes: number[]): UserSelection {
|
|
return {
|
|
...selection,
|
|
overrides: {
|
|
...selection.overrides,
|
|
zone_sizes: {
|
|
...selection.overrides.zone_sizes,
|
|
[groupId]: sizes,
|
|
}
|
|
}
|
|
};
|
|
}
|
|
|
|
/**
|
|
* 특정 섹션을 새로운 존으로 이동 (Drag & Drop)
|
|
*/
|
|
export function moveSectionToZone(
|
|
selection: UserSelection,
|
|
sectionId: string,
|
|
targetZoneId: string
|
|
): UserSelection {
|
|
const newZoneSections = { ...selection.overrides.zone_sections };
|
|
|
|
// 1. 모든 존에서 해당 섹션 제거 (이동 전 위치 클리어)
|
|
Object.keys(newZoneSections).forEach(zid => {
|
|
newZoneSections[zid] = newZoneSections[zid].filter(id => id !== sectionId);
|
|
});
|
|
|
|
// 2. 타겟 존에 섹션 추가
|
|
if (!newZoneSections[targetZoneId]) {
|
|
newZoneSections[targetZoneId] = [];
|
|
}
|
|
if (!newZoneSections[targetZoneId].includes(sectionId)) {
|
|
newZoneSections[targetZoneId].push(sectionId);
|
|
}
|
|
|
|
return {
|
|
...selection,
|
|
overrides: {
|
|
...selection.overrides,
|
|
zone_sections: newZoneSections
|
|
}
|
|
};
|
|
}
|
|
|
|
export function selectZone(selection: UserSelection, zoneId: string | null): UserSelection {
|
|
return {
|
|
...selection,
|
|
selectedZoneId: zoneId,
|
|
selectedRegionId: null, // Zone이 바뀌면 Region 선택 해제
|
|
};
|
|
}
|
|
|
|
export function selectRegion(selection: UserSelection, regionId: string | null): UserSelection {
|
|
return {
|
|
...selection,
|
|
selectedRegionId: regionId,
|
|
};
|
|
}
|
|
|
|
export function applyLayout(selection: UserSelection, layoutId: LayoutPresetId): UserSelection {
|
|
return {
|
|
...selection,
|
|
overrides: {
|
|
...selection.overrides,
|
|
layout_preset: layoutId,
|
|
},
|
|
};
|
|
}
|
|
|
|
export function applyFrame(selection: UserSelection, regionId: string, frameId: string): UserSelection {
|
|
return {
|
|
...selection,
|
|
overrides: {
|
|
...selection.overrides,
|
|
zone_frames: {
|
|
...selection.overrides.zone_frames,
|
|
[regionId]: frameId,
|
|
},
|
|
},
|
|
};
|
|
}
|
|
|
|
/**
|
|
* 현재 선택된 Zone 객체 반환
|
|
*/
|
|
export function getSelectedZone(slidePlan: SlidePlan | null, selection: UserSelection): Zone | null {
|
|
if (!slidePlan || !selection.selectedZoneId) return null;
|
|
// id 또는 zone_id 매칭
|
|
return slidePlan.zones.find(z => z.id === selection.selectedZoneId || z.zone_id === selection.selectedZoneId) || null;
|
|
}
|
|
|
|
/**
|
|
* 현재 선택된 Region 객체 반환
|
|
*/
|
|
export function getSelectedRegion(zone: Zone | null, selection: UserSelection): InternalRegion | null {
|
|
if (!zone || !selection.selectedRegionId) return null;
|
|
return zone.internal_regions.find(r => r.id === selection.selectedRegionId) || null;
|
|
}
|
|
|
|
/**
|
|
* 특정 Zone에 할당된 섹션 ID 목록 반환 (오버라이드 우선)
|
|
*/
|
|
export function getSectionsForZone(zone: Zone, selection: UserSelection): string[] {
|
|
return selection.overrides.zone_sections[zone.zone_id] || zone.section_ids;
|
|
}
|
|
|
|
/**
|
|
* 최종 유효 레이아웃 ID 반환
|
|
*/
|
|
export function getEffectiveLayoutId(slidePlan: SlidePlan | null, selection: UserSelection): LayoutPresetId {
|
|
if (selection.overrides.layout_preset) return selection.overrides.layout_preset;
|
|
return slidePlan?.layout_preset || 'single';
|
|
}
|