Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 27 additions & 3 deletions package/src/components/design-mode/output.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,9 @@ export function generateDesignOutput(
sorted.forEach((c, i) => {
const label = COMPONENT_MAP[c.type]?.label || c.type;
out += `${i + 1}. **${label}** — \`${Math.round(c.width)}×${Math.round(c.height)}px\` at \`(${Math.round(c.x)}, ${Math.round(c.y)})\`\n`;
if (c.text) {
out += ` - Note: "${c.text}"\n`;
}
});
return out;
}
Expand All @@ -121,6 +124,10 @@ export function generateDesignOutput(

out += `${i + 1}. **${label}** — \`${Math.round(c.width)}×${Math.round(c.height)}px\` at \`(${Math.round(c.x)}, ${Math.round(c.y)})\`\n`;

if (c.text) {
out += ` - Note: "${c.text}"\n`;
}

// Spatial context
const ctx = getSpatialContext(rect);
const includeLeftRight = detailLevel === "detailed" || detailLevel === "forensic";
Expand Down Expand Up @@ -272,8 +279,9 @@ export function generateRearrangeOutput(

const posMoved = Math.abs(o.x - c.x) > 1 || Math.abs(o.y - c.y) > 1;
const sizeChanged = Math.abs(o.width - c.width) > 1 || Math.abs(o.height - c.height) > 1;
const hasNote = !!s.note;

if (!posMoved && !sizeChanged) {
if (!posMoved && !sizeChanged && !hasNote) {
if (detailLevel === "forensic") {
changed.push({ section: s, posMoved: false, sizeChanged: false });
}
Expand All @@ -285,7 +293,11 @@ export function generateRearrangeOutput(

// Nothing changed
if (changed.length === 0) return "";
if (detailLevel !== "forensic" && changed.every((e) => !e.posMoved && !e.sizeChanged)) return "";
if (
detailLevel !== "forensic" &&
changed.every((e) => !e.posMoved && !e.sizeChanged && !e.section.note)
)
return "";

let out = "## Suggested Layout Changes\n\n";

Expand Down Expand Up @@ -319,7 +331,12 @@ export function generateRearrangeOutput(
const c = s.currentRect;

if (!posMoved && !sizeChanged) {
out += `- ${s.label} — unchanged at (${Math.round(c.x)}, ${Math.round(c.y)}) ${Math.round(c.width)}×${Math.round(c.height)}px\n`;
if (s.note) {
out += `- **${s.label}** — note only\n`;
out += ` - Note: "${s.note}"\n`;
} else {
out += `- ${s.label} — unchanged at (${Math.round(c.x)}, ${Math.round(c.y)}) ${Math.round(c.width)}×${Math.round(c.height)}px\n`;
}
continue;
}

Expand All @@ -332,6 +349,9 @@ export function generateRearrangeOutput(
} else {
out += `- Suggested: resize **${s.label}** to ${Math.round(c.width)}×${Math.round(c.height)}px\n`;
}
if (s.note) {
out += ` - Note: "${s.note}"\n`;
}
continue;
}

Expand All @@ -344,6 +364,10 @@ export function generateRearrangeOutput(
out += `- Suggested: resize **${s.label}** from ${Math.round(o.width)}×${Math.round(o.height)}px to ${Math.round(c.width)}×${Math.round(c.height)}px\n`;
}

if (s.note) {
out += ` - Note: "${s.note}"\n`;
}

if (posMoved) {
const origCtx = getSpatialContext(o, siblingCandidates("original"));
const currCtx = getSpatialContext(c, siblingCandidates("current"));
Expand Down
107 changes: 71 additions & 36 deletions package/src/components/page-toolbar-css/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -480,6 +480,7 @@ export function PageFeedbackToolbarCSS({

// Shadow annotation tracking (design → server sync)
const placementAnnotationMap = useRef(new Map<string, string>()); // placementId → server annotationId
const placementSyncedTextMap = useRef(new Map<string, string | undefined>()); // placementId → last-synced p.text
const rearrangeAnnotationMap = useRef(new Map<string, string>()); // sectionId → server annotationId
const rearrangeDebounceTimer = useRef<ReturnType<typeof originalSetTimeout>>();

Expand Down Expand Up @@ -998,6 +999,7 @@ const [settings, setSettings] = useState<ToolbarSettings>(() => {
for (const [placementId, annotationId] of placementAnnotationMap.current) {
if (annotationId === id) {
placementAnnotationMap.current.delete(placementId);
placementSyncedTextMap.current.delete(placementId);
setDesignPlacements((prev) => prev.filter((p) => p.id !== placementId));
break;
}
Expand Down Expand Up @@ -1340,56 +1342,83 @@ const [settings, setSettings] = useState<ToolbarSettings>(() => {
if (!endpoint || !currentSessionId) return;

const currentMap = placementAnnotationMap.current;
const syncedTextMap = placementSyncedTextMap.current;
const currentIds = new Set(designPlacements.map((p) => p.id));

// Create annotations for new placements
const buildPlacementComment = (p: DesignPlacement) =>
`Place ${p.type} at (${Math.round(p.x)}, ${Math.round(p.y)}), ${p.width}×${p.height}px${p.text ? ` — "${p.text}"` : ""}`;

// Create or update annotations for each placement
for (const p of designPlacements) {
if (currentMap.has(p.id)) continue;
if (!currentMap.has(p.id)) {
// Mark as in-flight to avoid duplicates
currentMap.set(p.id, "");
syncedTextMap.set(p.id, p.text);

const pageUrl =
typeof window !== "undefined"
? window.location.pathname + window.location.search + window.location.hash
: pathname;

syncAnnotation(endpoint, currentSessionId, {
id: p.id,
x: (p.x / window.innerWidth) * 100,
y: p.y,
comment: buildPlacementComment(p),
element: `[design:${p.type}]`,
elementPath: "[placement]",
timestamp: p.timestamp,
url: pageUrl,
intent: "change",
severity: "important",
kind: "placement",
placement: {
componentType: p.type,
width: p.width,
height: p.height,
scrollY: p.scrollY,
text: p.text,
},
} as Annotation)
.then((serverAnnotation) => {
// Update map with real server ID
if (currentMap.has(p.id)) {
currentMap.set(p.id, serverAnnotation.id);
}
})
.catch((err) => {
console.warn("[Agentation] Failed to sync placement annotation:", err);
currentMap.delete(p.id);
syncedTextMap.delete(p.id);
});
continue;
}

// Mark as in-flight to avoid duplicates
currentMap.set(p.id, "");
// Existing placement — push note updates once we have a server id
const existingId = currentMap.get(p.id);
if (!existingId) continue;
if (syncedTextMap.get(p.id) === p.text) continue;

const pageUrl =
typeof window !== "undefined"
? window.location.pathname + window.location.search + window.location.hash
: pathname;

syncAnnotation(endpoint, currentSessionId, {
id: p.id,
x: (p.x / window.innerWidth) * 100,
y: p.y,
comment: `Place ${p.type} at (${Math.round(p.x)}, ${Math.round(p.y)}), ${p.width}×${p.height}px${p.text ? ` — "${p.text}"` : ""}`,
element: `[design:${p.type}]`,
elementPath: "[placement]",
timestamp: p.timestamp,
url: pageUrl,
intent: "change",
severity: "important",
kind: "placement",
syncedTextMap.set(p.id, p.text);
updateAnnotationOnServer(endpoint, existingId, {
comment: buildPlacementComment(p),
placement: {
componentType: p.type,
width: p.width,
height: p.height,
scrollY: p.scrollY,
text: p.text,
},
} as Annotation)
.then((serverAnnotation) => {
// Update map with real server ID
if (currentMap.has(p.id)) {
currentMap.set(p.id, serverAnnotation.id);
}
})
.catch((err) => {
console.warn("[Agentation] Failed to sync placement annotation:", err);
currentMap.delete(p.id);
});
}).catch((err) => {
console.warn("[Agentation] Failed to update placement annotation:", err);
});
}

// Delete annotations for removed placements
for (const [placementId, annotationId] of currentMap) {
if (!currentIds.has(placementId)) {
currentMap.delete(placementId);
syncedTextMap.delete(placementId);
if (annotationId) {
deleteAnnotationFromServer(endpoint, annotationId).catch(() => {});
}
Expand Down Expand Up @@ -1435,8 +1464,8 @@ const [settings, setSettings] = useState<ToolbarSettings>(() => {
Math.abs(orig.width - curr.width) > 1 ||
Math.abs(orig.height - curr.height) > 1;

if (!hasMoved) {
// Section returned to original — delete annotation if exists
if (!hasMoved && !section.note) {
// Section returned to original with no note — delete annotation if exists
const existingId = currentMap.get(section.id);
if (existingId) {
currentMap.delete(section.id);
Expand All @@ -1445,11 +1474,16 @@ const [settings, setSettings] = useState<ToolbarSettings>(() => {
continue;
}

const notePart = section.note ? ` — "${section.note}"` : "";
const comment = hasMoved
? `Move ${section.label} section (${section.tagName}) — from (${Math.round(orig.x)},${Math.round(orig.y)}) ${Math.round(orig.width)}×${Math.round(orig.height)} to (${Math.round(curr.x)},${Math.round(curr.y)}) ${Math.round(curr.width)}×${Math.round(curr.height)}${notePart}`
: `Note on ${section.label} section (${section.tagName})${notePart}`;

const existingAnnotationId = currentMap.get(section.id);
if (existingAnnotationId) {
// Update existing
updateAnnotationOnServer(endpoint, existingAnnotationId, {
comment: `Move ${section.label} section (${section.tagName}) — from (${Math.round(orig.x)},${Math.round(orig.y)}) ${Math.round(orig.width)}×${Math.round(orig.height)} to (${Math.round(curr.x)},${Math.round(curr.y)}) ${Math.round(curr.width)}×${Math.round(curr.height)}`,
comment,
}).catch((err) => {
console.warn("[Agentation] Failed to update rearrange annotation:", err);
});
Expand All @@ -1461,7 +1495,7 @@ const [settings, setSettings] = useState<ToolbarSettings>(() => {
id: section.id,
x: (curr.x / window.innerWidth) * 100,
y: curr.y,
comment: `Move ${section.label} section (${section.tagName}) — from (${Math.round(orig.x)},${Math.round(orig.y)}) ${Math.round(orig.width)}×${Math.round(orig.height)} to (${Math.round(curr.x)},${Math.round(curr.y)}) ${Math.round(curr.width)}×${Math.round(curr.height)}`,
comment,
element: section.selector,
elementPath: "[rearrange]",
timestamp: Date.now(),
Expand Down Expand Up @@ -2894,6 +2928,7 @@ const [settings, setSettings] = useState<ToolbarSettings>(() => {
}
}
placementAnnotationMap.current.clear();
placementSyncedTextMap.current.clear();

// Delete shadow annotations for rearrange
for (const [, annotationId] of rearrangeAnnotationMap.current) {
Expand Down