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 +3 -2
- package/dist/chrome-client.js +33 -4
- package/dist/cli.mjs +104 -14
- package/package.json +1 -1
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
|
|
140
|
-
|
|
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.
|
package/dist/chrome-client.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
"
|
|
695
|
-
"
|
|
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
|
-
"
|
|
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
|
-
"
|
|
702
|
-
"Pass options such as tag, text, selector, target, or
|
|
703
|
-
"Call window.lavish.sendQueuedPrompts() when the control should immediately send
|
|
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
|
|
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
|
|
715
|
-
|
|
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
|
|
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(
|
|
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
|
-
|
|
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.
|
|
2097
|
+
var VERSION = "0.1.31";
|
|
2008
2098
|
async function run(argv) {
|
|
2009
2099
|
await ensureStateDir();
|
|
2010
2100
|
const normalizedArgv = normalizeArgv(argv);
|