@womp/mcp-ui 0.1.4 → 0.1.5

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.
@@ -0,0 +1,110 @@
1
+ import { $, createCard, type GenerationResult } from "../shared/bridge";
2
+
3
+ /**
4
+ * The order-status card — renders get_print_order (one order) and
5
+ * list_print_orders ({orders}) with the orders page's own status wording,
6
+ * colored by band: waiting / moving / done / stopped. A pending diff charge
7
+ * renders as the amber action strip so a blocked order explains itself.
8
+ * Read-only: no tool calls, no links out beyond what the model narrates.
9
+ */
10
+
11
+ interface OrderSummary {
12
+ orderId: number;
13
+ status: string;
14
+ totalCost: number;
15
+ discount: number;
16
+ items: { title: string; materialName: string; quantity: number }[];
17
+ tracking: { carrier: string; trackingNumber: string }[];
18
+ actionRequired?: { type: string; amount: number };
19
+ }
20
+
21
+ interface OrderPayload {
22
+ orders?: OrderSummary[];
23
+ orderId?: number;
24
+ }
25
+
26
+ const usd = (n: number) => `$${n.toFixed(2)}`;
27
+
28
+ /** The orders page's label set, mapped to a color band. */
29
+ function chipClass(status: string): string {
30
+ const s = status.toLowerCase();
31
+ if (s.includes("pending") || s.includes("review") || s.includes("action")) return "wait";
32
+ if (s.includes("printing") || s.includes("processing") || s.includes("way") || s.includes("returning"))
33
+ return "move";
34
+ if (s.includes("delivered")) return "done";
35
+ if (s.includes("canceled")) return "stop";
36
+ return "";
37
+ }
38
+
39
+ function orderBlock(order: OrderSummary): HTMLElement {
40
+ const block = document.createElement("div");
41
+ block.className = "order";
42
+
43
+ const head = document.createElement("div");
44
+ head.className = "order-head";
45
+ const id = document.createElement("div");
46
+ id.className = "order-id";
47
+ id.textContent = `Order #${order.orderId}`;
48
+ const chip = document.createElement("span");
49
+ chip.className = `chip ${chipClass(order.status)}`.trim();
50
+ chip.textContent = order.status;
51
+ head.append(id, chip);
52
+ block.append(head);
53
+
54
+ const lines = document.createElement("div");
55
+ lines.className = "lines";
56
+ (order.items || []).forEach((item) => {
57
+ const line = document.createElement("div");
58
+ line.textContent = `${item.quantity}x ${item.title} (${item.materialName})`;
59
+ lines.append(line);
60
+ });
61
+ block.append(lines);
62
+
63
+ const total = document.createElement("div");
64
+ total.className = "total-row";
65
+ const label = document.createElement("span");
66
+ label.textContent = order.discount > 0 ? `Total (after ${usd(order.discount)} discount)` : "Total";
67
+ const value = document.createElement("b");
68
+ value.textContent = usd(order.totalCost);
69
+ total.append(label, value);
70
+ block.append(total);
71
+
72
+ (order.tracking || []).forEach((t) => {
73
+ const track = document.createElement("div");
74
+ track.className = "tracking";
75
+ track.textContent = `Tracking: ${t.carrier ? `${t.carrier} ` : ""}${t.trackingNumber}`;
76
+ block.append(track);
77
+ });
78
+
79
+ if (order.actionRequired) {
80
+ const action = document.createElement("div");
81
+ action.className = "action";
82
+ action.textContent =
83
+ `Action required: the measured piece costs ${usd(order.actionRequired.amount)} more than quoted. ` +
84
+ `Approve the adjustment on womp.com under Profile -> Orders to unblock production.`;
85
+ block.append(action);
86
+ }
87
+
88
+ return block;
89
+ }
90
+
91
+ function render(data: OrderPayload) {
92
+ // list_print_orders sends {orders}; get_print_order sends one order flat.
93
+ const orders: OrderSummary[] = data.orders ?? (data.orderId ? [data as unknown as OrderSummary] : []);
94
+ const list = $("orders");
95
+ list.textContent = "";
96
+ orders.forEach((order) => list.append(orderBlock(order)));
97
+ $("empty").hidden = orders.length > 0;
98
+ $("status").textContent = "";
99
+ }
100
+
101
+ createCard("Womp orders", {
102
+ onResult: (raw: GenerationResult) => {
103
+ const data = raw as OrderPayload & GenerationResult;
104
+ if (data.status === "running") {
105
+ $("status").textContent = "Loading…";
106
+ return;
107
+ }
108
+ render(data);
109
+ },
110
+ });
@@ -181,7 +181,7 @@
181
181
  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
182
182
  <path d="M7 17 17 7M9 7h8v8" />
183
183
  </svg>
184
- <span id="order-label">Continue on womp.com</span>
184
+ <span id="order-label">Add to cart</span>
185
185
  </button>
186
186
  <div class="note" id="note" hidden>
187
187
  Shipping is added at checkout; the final price is confirmed before printing.
package/src/print/main.ts CHANGED
@@ -10,10 +10,12 @@ import { $, callTool, createCard, openLink, type GenerationResult } from "../sha
10
10
  * the tool with them: the host announces the call's arguments (`ontoolinput`)
11
11
  * before any result exists, and the controls initialize from that.
12
12
  *
13
- * Deliberately NO payment surface: "Continue on womp.com" calls
14
- * `prepare_print` (creates a free project) and opens the returned URL via
15
- * `ui/open-link` — address, shipping and payment all run in the browser flow
16
- * with every gate the web app already has.
13
+ * Deliberately NO payment surface: "Add to cart" calls the app-callable
14
+ * `add_to_cart` (creates a free project + a real cart item), and the button
15
+ * then becomes "Checkout on womp.com" — a `ui/open-link` to the cart checkout
16
+ * at /pay/<ids>, where address, shipping, promo codes and payment all run in
17
+ * the one existing cart UI. Turning any knob after an add resets the button
18
+ * to "Add to cart" so the checkout link always matches the shown settings.
17
19
  *
18
20
  * Static mode: a result that already carries `printUrl` (the model called
19
21
  * prepare_print directly) renders as a receipt — no controls, the button just
@@ -56,6 +58,8 @@ const orderBtn = $("order") as HTMLButtonElement;
56
58
  /** The mesh being quoted, and whether the card is a receipt or a configurator. */
57
59
  let source: { generationId?: number; glbUrl?: string } | undefined;
58
60
  let staticPrintUrl: string | undefined;
61
+ /** Set once the shown settings are in the cart; cleared by any knob turn. */
62
+ let cartCheckoutUrl = "";
59
63
  /** Monotonic guard: a slow older quote must never overwrite a newer one. */
60
64
  let quoteSeq = 0;
61
65
  let debounceTimer: number | undefined;
@@ -176,6 +180,10 @@ function scheduleRequote() {
176
180
  // Same idea as the web print panel's debounce: a slider being dragged must
177
181
  // not fire an engine estimate per pixel.
178
182
  if (debounceTimer !== undefined) clearTimeout(debounceTimer);
183
+ // The settings changed: whatever was added to the cart no longer matches
184
+ // what the card shows, so the button goes back to adding.
185
+ cartCheckoutUrl = "";
186
+ $("order-label").textContent = "Add to cart";
179
187
  debounceTimer = window.setTimeout(() => void requote(), 600);
180
188
  }
181
189
 
@@ -223,30 +231,33 @@ orderBtn.addEventListener("click", async () => {
223
231
  void openLink(card.app, staticPrintUrl);
224
232
  return;
225
233
  }
234
+ if (cartCheckoutUrl) {
235
+ void openLink(card.app, cartCheckoutUrl);
236
+ return;
237
+ }
226
238
  if (!source) return;
227
239
  orderBtn.disabled = true;
228
- $("order-label").textContent = "Setting up project…";
240
+ $("order-label").textContent = "Adding to cart…";
229
241
  setStatus("");
230
242
  try {
231
- const res = await callTool(card.app, "prepare_print", currentSettings());
232
- const data = (res.structuredContent || {}) as PrintQuoteResult;
233
- if (res.isError || !data.printUrl) {
234
- const text = (res.content as any[])?.find((c) => c.type === "text")?.text || "Could not set up the print.";
243
+ const res = await callTool(card.app, "add_to_cart", currentSettings());
244
+ const data = (res.structuredContent || {}) as { payUrl?: string; items?: unknown[]; subtotal?: number };
245
+ if (res.isError || !data.payUrl) {
246
+ const text = (res.content as any[])?.find((c) => c.type === "text")?.text || "Could not add to the cart.";
235
247
  setStatus(String(text), true);
248
+ $("order-label").textContent = "Add to cart";
236
249
  return;
237
250
  }
238
- // From here on the card is a receipt for the project that now exists —
239
- // silently re-quoting a project the user is already checking out would
240
- // desynchronize the two.
241
- staticPrintUrl = data.printUrl;
242
- $("controls").hidden = true;
243
- renderQuote(data);
244
- $("order-label").textContent = "Complete order on womp.com";
245
- void openLink(card.app, data.printUrl);
251
+ cartCheckoutUrl = data.payUrl;
252
+ $("order-label").textContent = "Checkout on womp.com";
253
+ setStatus(
254
+ `Added — cart has ${data.items?.length ?? 1} item(s), subtotal $${(data.subtotal ?? 0).toFixed(2)}. ` +
255
+ `Shipping and promo codes apply at checkout.`,
256
+ );
246
257
  } catch {
247
- setStatus("Could not set up the print — try again.", true);
258
+ setStatus("Could not add to the cart — try again.", true);
259
+ $("order-label").textContent = "Add to cart";
248
260
  } finally {
249
261
  orderBtn.disabled = false;
250
- if (!staticPrintUrl) $("order-label").textContent = "Continue on womp.com";
251
262
  }
252
263
  });