On imp47b-recovery: imp47b-frontend-u11-pre-rebase
This commit is contained in:
@@ -19,6 +19,20 @@ interface FramePanelProps {
|
|||||||
onNoDesignToggle: () => void;
|
onNoDesignToggle: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ─── IMP-41 u3 — application_mode consequence tooltip map (issue #70) ────────
|
||||||
|
// Keyed by application_mode VALUE (backend authoritative), NOT V4 label.
|
||||||
|
// Source = src/phase_z2_pipeline.py APPLICATION_MODE_BY_V4_LABEL (:107-112)
|
||||||
|
// emitted via Step 9 unit.application_candidates[] and forwarded by
|
||||||
|
// designAgentApi.ts (IMP-41 u2). When applicationMode is absent (legacy
|
||||||
|
// fixtures pre-IMP-32, or candidate filtered out at Step 9) the tooltip
|
||||||
|
// falls back to the raw V4 label string per Stage 2 contract.
|
||||||
|
const APPLICATION_MODE_TOOLTIP_KR: Record<string, string> = {
|
||||||
|
direct_insert: "코드 직접 적용",
|
||||||
|
same_frame_with_adjustment: "AI 보강 필요",
|
||||||
|
layout_or_region_change: "AI restructure 필요",
|
||||||
|
exclude: "render path 제외",
|
||||||
|
};
|
||||||
|
|
||||||
export default function FramePanel({
|
export default function FramePanel({
|
||||||
slidePlan,
|
slidePlan,
|
||||||
selectedZone,
|
selectedZone,
|
||||||
@@ -46,6 +60,26 @@ export default function FramePanel({
|
|||||||
return userSelection.overrides.zone_frames[targetRegion.id] || targetRegion.frame_match_strategy.frame_id;
|
return userSelection.overrides.zone_frames[targetRegion.id] || targetRegion.frame_match_strategy.frame_id;
|
||||||
}, [selectedZone, selectedRegion, userSelection.overrides.zone_frames]);
|
}, [selectedZone, selectedRegion, userSelection.overrides.zone_frames]);
|
||||||
|
|
||||||
|
// IMP-47B u11 — reject-click confirm guard. Per #76 policy: 사용자가 reject
|
||||||
|
// 카드 명시 클릭 → backend `--override-frame` 전달 + reject frame 유지 + AI 재구성.
|
||||||
|
// The window.confirm makes the AI-rebuild intent explicit (deselecting an
|
||||||
|
// already-applied reject frame does not prompt). Pure UX gate — no state
|
||||||
|
// mutation here; the parent `onFrameSelect` still owns the override apply.
|
||||||
|
const handleFrameSelect = React.useCallback(
|
||||||
|
(candidate: FrameCandidate) => {
|
||||||
|
const isReject = candidate.label === "reject";
|
||||||
|
const alreadyApplied = currentFrameId === candidate.id;
|
||||||
|
if (isReject && !alreadyApplied) {
|
||||||
|
const ok = window.confirm(
|
||||||
|
`"${candidate.name}" 은 V4 reject 라벨입니다.\n선택 시 frame 은 유지되고 AI 가 콘텐츠를 frame 구조에 맞게 재구성합니다.\n계속하시겠습니까?`,
|
||||||
|
);
|
||||||
|
if (!ok) return;
|
||||||
|
}
|
||||||
|
onFrameSelect(candidate.id);
|
||||||
|
},
|
||||||
|
[currentFrameId, onFrameSelect],
|
||||||
|
);
|
||||||
|
|
||||||
if (!selectedZone) {
|
if (!selectedZone) {
|
||||||
return (
|
return (
|
||||||
<div className="h-full flex flex-col items-center justify-center bg-slate-50 p-8 text-center text-slate-400">
|
<div className="h-full flex flex-col items-center justify-center bg-slate-50 p-8 text-center text-slate-400">
|
||||||
@@ -151,7 +185,7 @@ export default function FramePanel({
|
|||||||
className="w-full"
|
className="w-full"
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
onClick={() => onFrameSelect(candidate.id)}
|
onClick={() => handleFrameSelect(candidate)}
|
||||||
draggable
|
draggable
|
||||||
onDragStart={(e) => {
|
onDragStart={(e) => {
|
||||||
e.dataTransfer.setData("frameId", candidate.id);
|
e.dataTransfer.setData("frameId", candidate.id);
|
||||||
@@ -235,7 +269,19 @@ export default function FramePanel({
|
|||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
{/* V4 label badge */}
|
{/* V4 label badge */}
|
||||||
{candidate.label && (
|
{candidate.label && (() => {
|
||||||
|
// IMP-41 u3 — applicationMode-keyed Korean consequence
|
||||||
|
// tooltip with legacy fallback. applicationMode is
|
||||||
|
// forwarded by designAgentApi.ts (u2) from Step 9
|
||||||
|
// unit.application_candidates[]; undefined when the
|
||||||
|
// backend did not emit a mapping for this candidate.
|
||||||
|
const consequence = candidate.applicationMode
|
||||||
|
? APPLICATION_MODE_TOOLTIP_KR[candidate.applicationMode]
|
||||||
|
: undefined;
|
||||||
|
const badgeTitle = consequence
|
||||||
|
? `${consequence} (${candidate.applicationMode})`
|
||||||
|
: `V4 label: ${candidate.label}`;
|
||||||
|
return (
|
||||||
<span
|
<span
|
||||||
className={`text-[8px] font-black uppercase tracking-tight px-1.5 py-0.5 rounded ${
|
className={`text-[8px] font-black uppercase tracking-tight px-1.5 py-0.5 rounded ${
|
||||||
candidate.label === "use_as_is"
|
candidate.label === "use_as_is"
|
||||||
@@ -246,11 +292,12 @@ export default function FramePanel({
|
|||||||
? "bg-amber-100 text-amber-700"
|
? "bg-amber-100 text-amber-700"
|
||||||
: "bg-red-100 text-red-700"
|
: "bg-red-100 text-red-700"
|
||||||
}`}
|
}`}
|
||||||
title={`V4 label: ${candidate.label}`}
|
title={badgeTitle}
|
||||||
>
|
>
|
||||||
{candidate.label}
|
{candidate.label}
|
||||||
</span>
|
</span>
|
||||||
)}
|
);
|
||||||
|
})()}
|
||||||
{/* IMP-29 u3 — route hint chip (skip when direct_render = default). */}
|
{/* IMP-29 u3 — route hint chip (skip when direct_render = default). */}
|
||||||
{showRouteChip && (
|
{showRouteChip && (
|
||||||
<span
|
<span
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import {
|
|||||||
runPipeline,
|
runPipeline,
|
||||||
loadRun,
|
loadRun,
|
||||||
computeZonePositions,
|
computeZonePositions,
|
||||||
|
formatAiRepairHumanReviewMessage,
|
||||||
type RunMeta,
|
type RunMeta,
|
||||||
type PipelineOverrides,
|
type PipelineOverrides,
|
||||||
} from "../services/designAgentApi";
|
} from "../services/designAgentApi";
|
||||||
@@ -370,6 +371,13 @@ export default function Home() {
|
|||||||
}));
|
}));
|
||||||
setRunMeta(runMeta);
|
setRunMeta(runMeta);
|
||||||
toast.success(`run "${result.run_id}" 완료 — ${runMeta.status}`);
|
toast.success(`run "${result.run_id}" 완료 — ${runMeta.status}`);
|
||||||
|
// IMP-47B u11 — surface Step 12 AI repair failure axes (error /
|
||||||
|
// coverage_violated / unsupported_kind) as a human_review notification.
|
||||||
|
// Auto-pipeline first ([[feedback_auto_pipeline_first]]): no review_queue
|
||||||
|
// insertion — just an explicit error toast directing the user to pick
|
||||||
|
// another frame or edit manually. Helper returns null on success path.
|
||||||
|
const aiReviewMsg = formatAiRepairHumanReviewMessage(runMeta.ai_repair_status);
|
||||||
|
if (aiReviewMsg) toast.error(aiReviewMsg);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
toast.error(
|
toast.error(
|
||||||
|
|||||||
@@ -223,6 +223,35 @@ export interface FilteredSectionReason {
|
|||||||
position?: string | null;
|
position?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IMP-47B u11 — verbatim mirror of step20_slide_status.ai_repair_status (u8 schema).
|
||||||
|
// Surfaces Step 12 AI repair outcomes so the frontend can render a
|
||||||
|
// human_review notification when AI proposal validation, coverage, or call
|
||||||
|
// itself failed. Enum / field names kept verbatim — no frontend redefinition.
|
||||||
|
export interface AiRepairStatus {
|
||||||
|
status: "ok" | "applied" | "unsupported_kind" | "coverage_violated" | "error" | string;
|
||||||
|
counts: {
|
||||||
|
total: number;
|
||||||
|
applied: number;
|
||||||
|
no_proposal: number;
|
||||||
|
no_zone_match: number;
|
||||||
|
unsupported_kind: number;
|
||||||
|
error: number;
|
||||||
|
};
|
||||||
|
unsupported_kind_records: Array<{
|
||||||
|
unit_index?: number | null;
|
||||||
|
source_section_ids: string[];
|
||||||
|
apply_status: string;
|
||||||
|
}>;
|
||||||
|
error_records: Array<{
|
||||||
|
unit_index?: number | null;
|
||||||
|
source_section_ids: string[];
|
||||||
|
error: string;
|
||||||
|
}>;
|
||||||
|
coverage_status: string;
|
||||||
|
dropped_section_ids: string[];
|
||||||
|
human_review_required: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
export interface RunMeta {
|
export interface RunMeta {
|
||||||
run_id: string;
|
run_id: string;
|
||||||
mdx_path: string;
|
mdx_path: string;
|
||||||
@@ -237,6 +266,38 @@ export interface RunMeta {
|
|||||||
layout_candidates: string[]; // step07 layout_candidates list
|
layout_candidates: string[]; // step07 layout_candidates list
|
||||||
region_layout_candidates_by_zone: Record<string, string[]>; // step08 placeholder
|
region_layout_candidates_by_zone: Record<string, string[]>; // step08 placeholder
|
||||||
display_strategy_candidates_by_zone: Record<string, string[]>; // step08 placeholder
|
display_strategy_candidates_by_zone: Record<string, string[]>; // step08 placeholder
|
||||||
|
/** IMP-47B u11 — Step 12 AI repair outcome (u8 surfacing). null when
|
||||||
|
* step20 omits the field (legacy runs / pipeline aborted before Step 12). */
|
||||||
|
ai_repair_status: AiRepairStatus | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* IMP-47B u11 — Build the human_review notification text when Step 12 AI repair
|
||||||
|
* reports a failure axis. Returns null when no notification is needed (success,
|
||||||
|
* no AI invocation, or human_review_required=false). Pure function — no DOM, no
|
||||||
|
* toast side-effect — so it can be unit-tested without React Testing Library.
|
||||||
|
*
|
||||||
|
* Failure axes mapped to user-facing text (verbatim policy from
|
||||||
|
* IMP-47B #76 guardrail: "AI 호출 실패 / proposal validation 실패 / coverage 미달
|
||||||
|
* → frontend 에 명확한 notification").
|
||||||
|
*/
|
||||||
|
export function formatAiRepairHumanReviewMessage(
|
||||||
|
ai: AiRepairStatus | null | undefined,
|
||||||
|
): string | null {
|
||||||
|
if (!ai || !ai.human_review_required) return null;
|
||||||
|
if (ai.status === "error") {
|
||||||
|
const n = ai.counts?.error ?? ai.error_records?.length ?? 0;
|
||||||
|
return `AI 재구성 호출 실패 (${n}건) — 다른 frame 선택 또는 수동 편집 필요`;
|
||||||
|
}
|
||||||
|
if (ai.status === "coverage_violated") {
|
||||||
|
const dropped = (ai.dropped_section_ids || []).join(", ");
|
||||||
|
return `AI 재구성 후 콘텐츠 누락 (dropped: ${dropped || "?"}) — 다른 frame 선택 또는 수동 편집 필요`;
|
||||||
|
}
|
||||||
|
if (ai.status === "unsupported_kind") {
|
||||||
|
const n = ai.counts?.unsupported_kind ?? ai.unsupported_kind_records?.length ?? 0;
|
||||||
|
return `AI 제안 형식 미지원 (${n}건) — 다른 frame 선택 또는 수동 편집 필요`;
|
||||||
|
}
|
||||||
|
return `AI 재구성 human_review 필요 (status: ${ai.status})`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface LoadRunResult {
|
export interface LoadRunResult {
|
||||||
@@ -428,6 +489,7 @@ export async function loadRun(runId: string): Promise<LoadRunResult> {
|
|||||||
z.display_strategy_candidates ?? [],
|
z.display_strategy_candidates ?? [],
|
||||||
])
|
])
|
||||||
),
|
),
|
||||||
|
ai_repair_status: (slideStatus.data?.ai_repair_status ?? null) as AiRepairStatus | null,
|
||||||
};
|
};
|
||||||
|
|
||||||
// ── NormalizedContent ──
|
// ── NormalizedContent ──
|
||||||
@@ -527,9 +589,27 @@ export async function loadRun(runId: string): Promise<LoadRunResult> {
|
|||||||
if (lp !== 0) return lp;
|
if (lp !== 0) return lp;
|
||||||
return (b.confidence ?? 0) - (a.confidence ?? 0);
|
return (b.confidence ?? 0) - (a.confidence ?? 0);
|
||||||
});
|
});
|
||||||
|
// ─── IMP-41 u2 — application_candidates enrichment (issue #70) ───────────
|
||||||
|
// Backend Step 9 emits `unit.application_candidates[]` (src/phase_z2_pipeline.py
|
||||||
|
// _application_candidates_for_unit, :3071-3092) one entry per v4 candidate with
|
||||||
|
// application_mode / auto_applicable / delegated_to derived from
|
||||||
|
// APPLICATION_MODE_BY_V4_LABEL (:107-112). Enrichment ONLY — does NOT alter
|
||||||
|
// candidate source priority, sorting, or TOP_N_FRAMES slicing.
|
||||||
|
const applicationCandidates: any[] = Array.isArray(unit.application_candidates)
|
||||||
|
? unit.application_candidates
|
||||||
|
: [];
|
||||||
|
const applicationModeMap = new Map<string, any>();
|
||||||
|
applicationCandidates.forEach((ac: any) => {
|
||||||
|
const key = ac?.template_id;
|
||||||
|
if (typeof key === "string" && key.length > 0) {
|
||||||
|
applicationModeMap.set(key, ac);
|
||||||
|
}
|
||||||
|
});
|
||||||
const frameCandidates: FrameCandidate[] = v4Source
|
const frameCandidates: FrameCandidate[] = v4Source
|
||||||
.slice(0, TOP_N_FRAMES)
|
.slice(0, TOP_N_FRAMES)
|
||||||
.map((c: any) => ({
|
.map((c: any) => {
|
||||||
|
const appMatch = applicationModeMap.get(c.template_id);
|
||||||
|
return ({
|
||||||
id: c.template_id,
|
id: c.template_id,
|
||||||
name: c.template_id,
|
name: c.template_id,
|
||||||
score: c.confidence ?? 0,
|
score: c.confidence ?? 0,
|
||||||
@@ -559,7 +639,15 @@ export async function loadRun(runId: string): Promise<LoadRunResult> {
|
|||||||
decision: c.decision,
|
decision: c.decision,
|
||||||
reason: c.reason,
|
reason: c.reason,
|
||||||
capacityFit: c.capacity_fit,
|
capacityFit: c.capacity_fit,
|
||||||
}));
|
// ─── IMP-41 u2 — application_mode forwarding (issue #70) ───────────
|
||||||
|
// Source = unit.application_candidates[] indexed by template_id above.
|
||||||
|
// Optional fields — undefined when no matching application_candidate
|
||||||
|
// (legacy fixtures pre-IMP-32 or candidates filtered out at Step 9).
|
||||||
|
applicationMode: appMatch?.application_mode,
|
||||||
|
autoApplicable: appMatch?.auto_applicable,
|
||||||
|
delegatedTo: appMatch?.delegated_to ?? null,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
const displayStrategy = (
|
const displayStrategy = (
|
||||||
runMeta.display_strategy_candidates_by_zone[posEntry.name]?.[0] ??
|
runMeta.display_strategy_candidates_by_zone[posEntry.name]?.[0] ??
|
||||||
|
|||||||
@@ -175,6 +175,19 @@ export interface FrameCandidate {
|
|||||||
reason?: string | null;
|
reason?: string | null;
|
||||||
/** Capacity vs. content shape audit (compute_capacity_fit output). */
|
/** Capacity vs. content shape audit (compute_capacity_fit output). */
|
||||||
capacityFit?: CapacityFitEvidence | null;
|
capacityFit?: CapacityFitEvidence | null;
|
||||||
|
|
||||||
|
// ─── IMP-41 application_mode forwarding (issue #70 u1) ─────────────────────
|
||||||
|
// Source = src/phase_z2_pipeline.py APPLICATION_MODE_BY_V4_LABEL (:107-112),
|
||||||
|
// emitted by _application_candidates_for_unit() into Step 9
|
||||||
|
// unit.application_candidates[]. Optional — legacy fixtures pre-IMP-32 omit
|
||||||
|
// these and the FramePanel tooltip falls back to the raw V4 label.
|
||||||
|
|
||||||
|
/** Application mode mapped from V4 label by backend (authoritative). */
|
||||||
|
applicationMode?: 'direct_insert' | 'same_frame_with_adjustment' | 'layout_or_region_change' | 'exclude';
|
||||||
|
/** True when backend marks the candidate as automatically applicable. */
|
||||||
|
autoApplicable?: boolean;
|
||||||
|
/** Delegation target step / actor (e.g. "step10_contract_check", "human_review"). */
|
||||||
|
delegatedTo?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─────────────────────────────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
|||||||
Reference in New Issue
Block a user