@womp/mcp-ui 0.1.19 → 0.1.21
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/dist/index.d.ts +4 -0
- package/dist/index.js +18 -9
- package/dist/openai-camera/index.html +423 -173
- package/dist/openai-order/index.html +210 -0
- package/package.json +4 -3
- package/src/openai-camera/index.html +416 -166
- package/src/openai-camera/main.ts +289 -51
- package/src/openai-order/index.html +136 -0
- package/src/openai-order/main.ts +137 -0
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { $, createCard, openExternal, type GenerationResult } from "../shared/bridge";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The order card: what `get_order_status` answers on the OpenAI MCP (swan-backend
|
|
5
|
+
* docs/OPENAI_MCP.md) when the user asks where their camera is. The order's
|
|
6
|
+
* step on a Paid → Printing → Shipped → Delivered tracker, when it should
|
|
7
|
+
* arrive, and a button to its status page on the web.
|
|
8
|
+
*
|
|
9
|
+
* The server found the order by the ChatGPT user's checkout links, and a link
|
|
10
|
+
* can be forwarded, so what arrives says nothing that reaches the buyer: no
|
|
11
|
+
* name, city or money. Track opens the order's status page — the masked view
|
|
12
|
+
* its emails link — and is all the card does.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
type Step = "paid" | "printing" | "shipped" | "delivered" | "canceled";
|
|
16
|
+
|
|
17
|
+
interface OrderView {
|
|
18
|
+
id: number;
|
|
19
|
+
statusLabel: string;
|
|
20
|
+
step: Step;
|
|
21
|
+
placedAt: string;
|
|
22
|
+
items: { title: string; summary: string; quantity: number; swatches: { shell: string[]; button: string[] } }[];
|
|
23
|
+
/** Each stage's date, done or expected (ISO); null when it won't arrive (canceled, returning) or can't be dated. */
|
|
24
|
+
timeline: { deliveredAt: string } | null;
|
|
25
|
+
statusUrl: string | null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
interface Payload {
|
|
29
|
+
/** Absent when nothing is paid for yet, or the number asked for isn't theirs. */
|
|
30
|
+
order?: OrderView;
|
|
31
|
+
orders: { id: number }[];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const TRACKER: Step[] = ["paid", "printing", "shipped", "delivered"];
|
|
35
|
+
|
|
36
|
+
/** The order number the model asked for, if any: the empty state names it. */
|
|
37
|
+
let askedFor: number | undefined;
|
|
38
|
+
let order: OrderView | undefined;
|
|
39
|
+
|
|
40
|
+
const setStatus = (text: string) => {
|
|
41
|
+
$("status").textContent = text;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
/** One colour fills; two or three run top to bottom, as on the shell. */
|
|
45
|
+
const paint = (colors: string[]) => (colors.length > 1 ? `linear-gradient(180deg, ${colors.join(", ")})` : colors[0] || "transparent");
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* "Nov 4". The server dates the stages in calendar days at UTC midnight, so
|
|
49
|
+
* they are read in UTC: in the user's own zone, west of it, they would be a
|
|
50
|
+
* day early.
|
|
51
|
+
*/
|
|
52
|
+
function day(iso: string) {
|
|
53
|
+
const date = new Date(iso);
|
|
54
|
+
return date.toLocaleDateString("en-US", {
|
|
55
|
+
month: "short",
|
|
56
|
+
day: "numeric",
|
|
57
|
+
timeZone: "UTC",
|
|
58
|
+
...(date.getUTCFullYear() !== new Date().getUTCFullYear() ? { year: "numeric" } : {}),
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** "womp.com" from the link, so the button names where it goes on every environment. */
|
|
63
|
+
function host(url: string) {
|
|
64
|
+
try {
|
|
65
|
+
return new URL(url).hostname.replace(/^www\./, "");
|
|
66
|
+
} catch {
|
|
67
|
+
return "the web";
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function renderEmpty() {
|
|
72
|
+
$("head").hidden = false;
|
|
73
|
+
$("thumb").style.background = "";
|
|
74
|
+
$("cap").removeAttribute("style");
|
|
75
|
+
$("title").textContent = askedFor !== undefined ? `No order #${askedFor} here` : "No order yet";
|
|
76
|
+
$("line").textContent = "Orders paid for from this chat's checkout links show up here.";
|
|
77
|
+
$("tracker").hidden = true;
|
|
78
|
+
$("track").hidden = true;
|
|
79
|
+
setStatus("");
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function render(o: OrderView) {
|
|
83
|
+
const item = o.items[0];
|
|
84
|
+
$("thumb").style.background = item ? paint(item.swatches.shell) : "";
|
|
85
|
+
if (item) $("cap").style.background = paint(item.swatches.button);
|
|
86
|
+
else $("cap").removeAttribute("style");
|
|
87
|
+
|
|
88
|
+
$("title").textContent = o.step === "printing" ? "Printing now" : o.statusLabel;
|
|
89
|
+
const when =
|
|
90
|
+
o.step === "canceled" || !o.timeline
|
|
91
|
+
? ""
|
|
92
|
+
: o.step === "delivered"
|
|
93
|
+
? ` · Delivered ${day(o.timeline.deliveredAt)}`
|
|
94
|
+
: ` · Arrives around ${day(o.timeline.deliveredAt)}`;
|
|
95
|
+
$("line").textContent = `Order #${o.id}${when}`;
|
|
96
|
+
$("head").hidden = false;
|
|
97
|
+
|
|
98
|
+
// A canceled order has no place on the way to the door.
|
|
99
|
+
const reached = TRACKER.indexOf(o.step);
|
|
100
|
+
$("tracker").hidden = reached < 0;
|
|
101
|
+
for (const li of Array.from($("tracker").children) as HTMLElement[]) {
|
|
102
|
+
const at = TRACKER.indexOf(li.dataset.step as Step);
|
|
103
|
+
li.classList.toggle("done", at <= reached);
|
|
104
|
+
li.classList.toggle("now", at === reached);
|
|
105
|
+
if (at === reached) li.setAttribute("aria-current", "step");
|
|
106
|
+
else li.removeAttribute("aria-current");
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const track = $("track") as HTMLButtonElement;
|
|
110
|
+
track.hidden = !o.statusUrl;
|
|
111
|
+
if (o.statusUrl) track.textContent = `Track on ${host(o.statusUrl)} ↗`;
|
|
112
|
+
setStatus("");
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const card = createCard("Order", {
|
|
116
|
+
onInput: (args) => {
|
|
117
|
+
askedFor = typeof args.orderId === "number" ? args.orderId : undefined;
|
|
118
|
+
setStatus("Checking your order…");
|
|
119
|
+
},
|
|
120
|
+
onResult: (raw: GenerationResult) => {
|
|
121
|
+
const data = raw as unknown as Partial<Payload>;
|
|
122
|
+
// The bridge also calls this with a bare { status: "running" } placeholder.
|
|
123
|
+
if (!Array.isArray(data.orders)) return;
|
|
124
|
+
order = data.order;
|
|
125
|
+
if (order) render(order);
|
|
126
|
+
else renderEmpty();
|
|
127
|
+
},
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
$("track").addEventListener("click", async () => {
|
|
131
|
+
if (!order?.statusUrl) return;
|
|
132
|
+
try {
|
|
133
|
+
await openExternal(card.app, order.statusUrl);
|
|
134
|
+
} catch {
|
|
135
|
+
setStatus("Couldn't open it here. Ask in the chat for the order's status page.");
|
|
136
|
+
}
|
|
137
|
+
});
|