diff --git a/package/src/components/design-mode/output.ts b/package/src/components/design-mode/output.ts index 59c2b975..4969376d 100644 --- a/package/src/components/design-mode/output.ts +++ b/package/src/components/design-mode/output.ts @@ -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; } @@ -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"; @@ -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 }); } @@ -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"; @@ -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; } @@ -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; } @@ -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")); diff --git a/package/src/components/page-toolbar-css/index.tsx b/package/src/components/page-toolbar-css/index.tsx index 42df435a..7c2ec00e 100644 --- a/package/src/components/page-toolbar-css/index.tsx +++ b/package/src/components/page-toolbar-css/index.tsx @@ -480,6 +480,7 @@ export function PageFeedbackToolbarCSS({ // Shadow annotation tracking (design → server sync) const placementAnnotationMap = useRef(new Map()); // placementId → server annotationId + const placementSyncedTextMap = useRef(new Map()); // placementId → last-synced p.text const rearrangeAnnotationMap = useRef(new Map()); // sectionId → server annotationId const rearrangeDebounceTimer = useRef>(); @@ -998,6 +999,7 @@ const [settings, setSettings] = useState(() => { 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; } @@ -1340,32 +1342,66 @@ const [settings, setSettings] = useState(() => { 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, @@ -1373,23 +1409,16 @@ const [settings, setSettings] = useState(() => { 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(() => {}); } @@ -1435,8 +1464,8 @@ const [settings, setSettings] = useState(() => { 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); @@ -1445,11 +1474,16 @@ const [settings, setSettings] = useState(() => { 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); }); @@ -1461,7 +1495,7 @@ const [settings, setSettings] = useState(() => { 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(), @@ -2894,6 +2928,7 @@ const [settings, setSettings] = useState(() => { } } placementAnnotationMap.current.clear(); + placementSyncedTextMap.current.clear(); // Delete shadow annotations for rearrange for (const [, annotationId] of rearrangeAnnotationMap.current) {