lavish-axi 0.1.30 → 0.1.31

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -136,8 +136,9 @@ pnpm link
136
136
  That fallback guidance recommends DaisyUI's `luxury` theme by default and warns not to `@apply` DaisyUI classes inside Tailwind browser-runtime style blocks.
137
137
  - **Local assets** - Copy local images, CSS, fonts, and scripts next to the HTML artifact and reference them with relative paths from that directory; root-prefixed paths such as `/assets/logo.png` will not resolve through Lavish's artifact route.
138
138
  - **Live reload** - Lavish watches the HTML artifact file by default and preserves the artifact iframe scroll position across reloads. To also reload on sibling asset changes, add `data-lavish-live-reload-root` to the root element or `<meta name="lavish-live-reload" content="root">`.
139
- - **Feedback controls** - Native form controls (radios, checkboxes, inputs, selects, buttons, labels, contenteditable) are interactive automatically, so they do not need `data-lavish-action`; wire their handlers to `window.lavish.queuePrompt()` or `window.lavish.sendQueuedPrompts()` to send feedback.
140
- Mark only custom (non-native) clickable elements with `data-lavish-action` so Lavish does not annotate them.
139
+ - **Feedback controls** - Native form controls (radios, checkboxes, inputs, selects, buttons, labels, contenteditable) are interactive automatically, so they do not need `data-lavish-action`.
140
+ For reversible choices, let option clicks update local state, then queue exactly one final answer from a per-question submit or Queue answer button with `window.lavish.queuePrompt()`.
141
+ Mark only custom (non-native) clickable elements with `data-lavish-action` so Lavish does not annotate them, and use `data-lavish-question` or `queueKey` when pre-send updates for the same question should replace each other.
141
142
  The browser chrome keeps editing actions in the overflow menu (copy path, reload artifact, copy DOM snapshot, end session) and can submit queued prompts with **Send & end session**, which delivers the prompts before ending the session.
142
143
  - **Keyboard shortcuts** - In the chrome composer, Enter sends queued prompts and Shift+Enter inserts a newline.
143
144
  In the annotation card, Enter queues the annotation, Shift+Enter inserts a newline, and Ctrl+Enter (Cmd+Enter on macOS) queues it and sends all queued prompts immediately.
@@ -5,6 +5,7 @@ const sessionData = JSON.parse(sessionDataElement?.textContent || "{}");
5
5
  const key = String(sessionData.key || "");
6
6
  const filePath = String(sessionData.file || "");
7
7
  const queueStorageKey = "lavish-axi:queued:" + key;
8
+ const internalQueueKeyField = "_lavishQueueKey";
8
9
  const initialChat = Array.isArray(sessionData.initialChat) ? sessionData.initialChat : [];
9
10
 
10
11
  const frame = /** @type {HTMLIFrameElement} */ (document.getElementById("artifact"));
@@ -212,6 +213,36 @@ function removeQueuedPrompt(index, event) {
212
213
  render();
213
214
  }
214
215
 
216
+ function promptQueueKey(prompt) {
217
+ return prompt && typeof prompt[internalQueueKeyField] === "string" ? prompt[internalQueueKeyField].trim() : "";
218
+ }
219
+
220
+ function enqueuePrompt(prompt) {
221
+ if (!prompt || typeof prompt !== "object") return;
222
+
223
+ const queueKey = promptQueueKey(prompt);
224
+ if (queueKey) {
225
+ const index = queued.findIndex((item) => promptQueueKey(item) === queueKey);
226
+ if (index !== -1) {
227
+ queued[index] = prompt;
228
+ } else {
229
+ queued.push(prompt);
230
+ }
231
+ } else {
232
+ queued.push(prompt);
233
+ }
234
+
235
+ persistQueuedPrompts();
236
+ render();
237
+ }
238
+
239
+ function stripInternalPromptFields(prompt) {
240
+ if (!prompt || typeof prompt !== "object") return prompt;
241
+ const clean = { ...prompt };
242
+ delete clean[internalQueueKeyField];
243
+ return clean;
244
+ }
245
+
215
246
  function postToFrame(message) {
216
247
  if (frame.contentWindow) frame.contentWindow.postMessage(message, "*");
217
248
  }
@@ -279,7 +310,7 @@ async function submitQueuedOnce() {
279
310
  const response = await fetch("/api/" + key + "/prompts", {
280
311
  method: "POST",
281
312
  headers: { "content-type": "application/json" },
282
- body: JSON.stringify({ prompts, domSnapshot: pendingSnapshot }),
313
+ body: JSON.stringify({ prompts: prompts.map(stripInternalPromptFields), domSnapshot: pendingSnapshot }),
283
314
  });
284
315
  if (!response.ok) throw new Error("failed to submit queued prompts");
285
316
  for (const prompt of prompts) {
@@ -359,9 +390,7 @@ window.addEventListener("message", (event) => {
359
390
 
360
391
  const msg = event.data || {};
361
392
  if (msg.type === "lavish:queuePrompt") {
362
- queued.push(msg.prompt);
363
- persistQueuedPrompts();
364
- render();
393
+ enqueuePrompt(msg.prompt);
365
394
  }
366
395
  if (msg.type === "lavish:snapshot") {
367
396
  const snapshotAction = snapshotRequests.shift() || "submit";
package/dist/cli.mjs CHANGED
@@ -691,28 +691,31 @@ var PLAYBOOKS = [
691
691
  ],
692
692
  structure: [
693
693
  "Make each decision surface visible: what is being chosen, what the options mean, and what happens next.",
694
- "Pair controls with a readable summary of the prompt they will queue.",
695
- "Show queued or selected state clearly so the user trusts what will be sent back."
694
+ "Keep reversible selection state local in the artifact until the user explicitly submits that question.",
695
+ "Pair each question with a Submit or Queue answer control that sends exactly one prompt for the final answer.",
696
+ "Show selected state separately from queued state so the user trusts what will be sent back."
696
697
  ],
697
698
  design_rules: [
698
699
  "Native form controls - radios, checkboxes, text inputs, selects, textareas, buttons, options, labels, and contenteditable regions - are interactive automatically: clicks toggle, focus, and type instead of annotating, so they do not need data-lavish-action. Build choice and option UIs from these whenever you can.",
699
- "To turn a native control into agent feedback, wire its own change, click, or submit handler to window.lavish.queuePrompt() (for example a radio group's onchange, or a submit button that reads the selected values) - no marker required.",
700
+ "For reversible choices, do not call window.lavish.queuePrompt() from radio change handlers or option click handlers. Those handlers should only update local selected state.",
701
+ "Use a per-question form submit or explicit Queue answer button to read the current values and call window.lavish.queuePrompt() exactly once for the final answer.",
700
702
  "Put data-lavish-action only on custom (non-native) elements that should act like a feedback control - typically a styled div or span you made clickable - so Lavish does not annotate it and shows a pointer cursor instead.",
701
- "Call window.lavish.queuePrompt(prompt, options) from the control's click, change, or submit handler to queue a precise request for the agent.",
702
- "Pass options such as tag, text, selector, target, or data when they help the agent understand exactly what the user chose.",
703
- "Call window.lavish.sendQueuedPrompts() when the control should immediately send the queued feedback instead of waiting for the user to press Send to Agent.",
703
+ "Use data-lavish-question on a question wrapper or pass queueKey when multiple pre-send updates should replace the prior unsent answer for the same question.",
704
+ "Pass options such as tag, text, selector, target, data, queueKey, or element when they help the agent understand exactly what the user chose.",
705
+ "Call window.lavish.sendQueuedPrompts() only when the control should immediately send committed feedback instead of waiting for the user to press Send to Agent.",
704
706
  "Make queued prompts specific enough that the agent can act without asking a follow-up question.",
705
707
  "Keep native browser controls accessible and readable on mobile."
706
708
  ],
707
709
  pitfalls: [
710
+ "Do not queue one prompt per radio change, checkbox toggle, dropdown change, or choice-button click when the user can still change their mind.",
708
711
  "Do not create controls whose queued prompt is unclear or too vague to execute.",
709
- "Do not hide the fact that a click queues feedback for the agent.",
712
+ "Do not hide the difference between selected locally and queued for the agent.",
710
713
  "Do not require interaction for content the user only needs to read."
711
714
  ],
712
715
  lavish_notes: [
713
716
  "Lavish is strongest when the artifact becomes a focused review surface and not just a static page.",
714
- `A native choice looks like \`<label><input type="radio" name="plan" onchange="window.lavish.queuePrompt('Use the Pro plan', { tag: 'choice', text: 'Pro' })"> Pro</label>\` - it just works, no marker needed.`,
715
- `A custom (non-native) control needs the marker: \`<div role="button" tabindex="0" data-lavish-action onclick="window.lavish.queuePrompt('Choose option A', { tag: 'choice', text: 'Option A' })">Choose option A</div>\`.`,
717
+ `A native single-choice question should submit the final value: \`<form data-lavish-question="plan" onsubmit="event.preventDefault(); const choice = new FormData(event.currentTarget).get('plan'); if (choice) window.lavish.queuePrompt('Use the ' + choice + ' plan', { tag: 'choice', text: 'Plan: ' + choice, element: event.currentTarget, data: { question: 'plan', answer: choice } });"><label><input type="radio" name="plan" value="Starter"> Starter</label><label><input type="radio" name="plan" value="Pro"> Pro</label><button type="submit">Queue this answer</button></form>\`.`,
718
+ "A custom choice UI should make option buttons update local state, then use a separate Queue answer button with data-lavish-action to queue the final selected value.",
716
719
  "Use window.lavish.queuePrompt for user intent, not internal analytics or UI-only state changes.",
717
720
  "End input paths with an obvious way for the user to send feedback back to the agent."
718
721
  ]
@@ -765,7 +768,90 @@ import chokidar from "chokidar";
765
768
  import express from "express";
766
769
 
767
770
  // src/artifact-sdk.js
768
- function createArtifactSdk() {
771
+ function deriveLavishQueueKey(element, options = {}) {
772
+ function stringValue(value) {
773
+ return value === null || value === void 0 ? "" : String(value);
774
+ }
775
+ function attributeValue(el, name) {
776
+ if (!el) return "";
777
+ if (el.getAttribute) {
778
+ const value = el.getAttribute(name);
779
+ if (value !== null && value !== void 0) return value;
780
+ }
781
+ return el[name] || "";
782
+ }
783
+ function tagName(el) {
784
+ return stringValue(el?.tagName || el?.nodeName).toLowerCase();
785
+ }
786
+ function closestElementMatching(el, selector) {
787
+ return el && el.closest ? el.closest(selector) : null;
788
+ }
789
+ function elementPath(el) {
790
+ const parts = [];
791
+ let node = el;
792
+ while (node && node.nodeType === 1 && parts.length < 6) {
793
+ let part = tagName(node) || "element";
794
+ const id = stringValue(attributeValue(node, "id") || node.id).trim();
795
+ if (id) {
796
+ part += `#${id}`;
797
+ parts.unshift(part);
798
+ break;
799
+ }
800
+ const parent2 = node.parentElement;
801
+ if (parent2 && parent2.children) {
802
+ const siblings = [...parent2.children].filter((child) => tagName(child) === tagName(node));
803
+ if (siblings.length > 1) part += `:nth-of-type(${siblings.indexOf(node) + 1})`;
804
+ }
805
+ parts.unshift(part);
806
+ node = parent2;
807
+ }
808
+ return parts.join(" > ");
809
+ }
810
+ function scopeKey(el) {
811
+ const scope2 = closestElementMatching(el, "form,fieldset") || el?.parentElement || el;
812
+ const tag2 = tagName(scope2) || "scope";
813
+ const explicit = stringValue(
814
+ attributeValue(scope2, "data-lavish-question") || attributeValue(scope2, "id") || attributeValue(scope2, "name")
815
+ ).trim();
816
+ if (explicit) return `${tag2}:${explicit}`;
817
+ return elementPath(scope2) || tag2;
818
+ }
819
+ function controlIdentity(el) {
820
+ const identity = stringValue(attributeValue(el, "name") || attributeValue(el, "id") || el?.name).trim();
821
+ if (identity) return identity;
822
+ return elementPath(el);
823
+ }
824
+ function isKeyedInputType(type2) {
825
+ return !(/* @__PURE__ */ new Set(["button", "submit", "reset", "file", "image", "hidden", "radio", "checkbox"])).has(type2);
826
+ }
827
+ if (Object.hasOwn(options, "queueKey")) {
828
+ return stringValue(options.queueKey).trim();
829
+ }
830
+ const question = closestElementMatching(element, "[data-lavish-question]");
831
+ const questionKey = stringValue(attributeValue(question, "data-lavish-question")).trim();
832
+ if (questionKey) return `question:${questionKey}`;
833
+ const tag = tagName(element);
834
+ const type = stringValue(attributeValue(element, "type") || element?.type).toLowerCase();
835
+ const scope = scopeKey(element);
836
+ if (tag === "input" && type === "radio") {
837
+ const name = stringValue(attributeValue(element, "name") || element?.name).trim();
838
+ if (name) return `radio:${scope}:${name}`;
839
+ return "";
840
+ }
841
+ if (tag === "input" && type === "checkbox") {
842
+ const identity = controlIdentity(element);
843
+ const explicitValue = stringValue(element?.getAttribute ? element.getAttribute("value") : "").trim();
844
+ const option = explicitValue || stringValue(attributeValue(element, "id") || elementPath(element)).trim();
845
+ if (identity) return `checkbox:${scope}:${identity}:${option}`;
846
+ return "";
847
+ }
848
+ if (tag === "select" || tag === "textarea" || tag === "input" && isKeyedInputType(type)) {
849
+ const identity = controlIdentity(element);
850
+ if (identity) return `field:${scope}:${identity}`;
851
+ }
852
+ return "";
853
+ }
854
+ function createArtifactSdk(deriveQueueKey) {
769
855
  let annotationMode = true;
770
856
  let hovered = null;
771
857
  let selected = null;
@@ -904,10 +990,13 @@ function createArtifactSdk() {
904
990
  if (!annotationMode) closeCard();
905
991
  }
906
992
  function queuePrompt(prompt, options = {}) {
993
+ const originElement = options.element || document.activeElement || document.body;
907
994
  const item = {
908
- ...context(options.element || document.activeElement || document.body),
995
+ ...context(originElement),
909
996
  prompt: String(prompt || "")
910
997
  };
998
+ const queueKey = typeof deriveQueueKey === "function" ? deriveQueueKey(originElement, options) : "";
999
+ if (queueKey) item._lavishQueueKey = String(queueKey);
911
1000
  if (options.uid) item.uid = String(options.uid);
912
1001
  if (options.selector) item.selector = String(options.selector);
913
1002
  if (options.tag) item.tag = String(options.tag);
@@ -993,7 +1082,7 @@ function createArtifactSdk() {
993
1082
  cancelButton.onclick = closeCard;
994
1083
  sendButton.onclick = () => {
995
1084
  const prompt = textarea.value.trim();
996
- if (prompt) queuePrompt(prompt, c);
1085
+ if (prompt) queuePrompt(prompt, { ...c, queueKey: "" });
997
1086
  closeCard();
998
1087
  };
999
1088
  textarea.addEventListener("keydown", (event) => {
@@ -1819,7 +1908,8 @@ function createSdkJs(key) {
1819
1908
  return `(() => {
1820
1909
  const key=${JSON.stringify(key)};
1821
1910
  void key;
1822
- (${createArtifactSdk.toString()})();
1911
+ const deriveQueueKey=${deriveLavishQueueKey.toString()};
1912
+ (${createArtifactSdk.toString()})(deriveQueueKey);
1823
1913
  })();`;
1824
1914
  }
1825
1915
  function escapeHtml(value) {
@@ -2004,7 +2094,7 @@ function normalizePagePath(path5) {
2004
2094
  // src/cli.js
2005
2095
  var COMMANDS = /* @__PURE__ */ new Set(["open", "poll", "end", "stop", "server", "playbook", "design", "setup"]);
2006
2096
  var DESCRIPTION = "Lavish Editor helps agents turn rich HTML artifacts into collaborative human review surfaces. Whenever you are about to give user a complex response that will be easier to understand via a rich / interactive page, consider using Lavish Editor. First generate an interactive HTML artifact according to user request, then run `lavish-axi <html-file>` so the user can visually review it, annotate elements or selected text, queue prompts, and send feedback back through `lavish-axi poll`.";
2007
- var VERSION = "0.1.30";
2097
+ var VERSION = "0.1.31";
2008
2098
  async function run(argv) {
2009
2099
  await ensureStateDir();
2010
2100
  const normalizedArgv = normalizeArgv(argv);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lavish-axi",
3
- "version": "0.1.30",
3
+ "version": "0.1.31",
4
4
  "packageManager": "pnpm@11.1.1",
5
5
  "description": "HTML is the new markdown. Lavish is the new editor for your HTML artifacts.",
6
6
  "type": "module",