feat(frontend): add Front/ — Vite/React frontend with backend pipeline integration

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.
This commit is contained in:
2026-05-14 14:48:42 +09:00
parent 52ccb7fc8b
commit 0f0d3fa91f
99 changed files with 20280 additions and 0 deletions
+176
View File
@@ -0,0 +1,176 @@
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';
}