@tmls-ai/support 0.1.0 → 0.1.2

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.
Files changed (2) hide show
  1. package/dist/index.js +59 -4
  2. package/package.json +26 -7
package/dist/index.js CHANGED
@@ -75,6 +75,16 @@ var SupportApi = class {
75
75
  const res = await fetch(`${this.apiUrl}/v1/conversations/${conversationId}/messages`, { headers: await this.auth() });
76
76
  return res.ok ? res.json() : [];
77
77
  }
78
+ /** Full thread: meta (type/severity/status) + messages + attachments. */
79
+ async getConversation(conversationId) {
80
+ const res = await fetch(`${this.apiUrl}/v1/conversations/${conversationId}`, { headers: await this.auth() });
81
+ return res.ok ? res.json() : null;
82
+ }
83
+ /** Fetch a screenshot the user owns; returns an object URL (auth header can't ride an <img src>). */
84
+ async getScreenshot(r2Key) {
85
+ const res = await fetch(`${this.apiUrl}/v1/screenshots/${r2Key}`, { headers: await this.auth() });
86
+ return res.ok ? URL.createObjectURL(await res.blob()) : null;
87
+ }
78
88
  async reply(conversationId, body) {
79
89
  await fetch(`${this.apiUrl}/v1/conversations/${conversationId}/messages`, {
80
90
  method: "POST",
@@ -110,14 +120,23 @@ function SupportWidget(props) {
110
120
  const [lastTicket, setLastTicket] = useState(null);
111
121
  const [threads, setThreads] = useState([]);
112
122
  const [activeId, setActiveId] = useState(null);
123
+ const [activeConvo, setActiveConvo] = useState(null);
113
124
  const [messages, setMessages] = useState([]);
125
+ const [shotUrls, setShotUrls] = useState({});
114
126
  const [reply, setReply] = useState("");
115
127
  useEffect(() => {
116
128
  installErrorCapture();
117
129
  }, []);
118
130
  const captureShot = useCallback(async () => {
119
131
  try {
120
- const blob = await domToBlob(document.body, { quality: 0.8, scale: 0.75 });
132
+ const root = document.documentElement;
133
+ const blob = await domToBlob(root, {
134
+ quality: 0.8,
135
+ scale: 0.75,
136
+ width: root.clientWidth || window.innerWidth,
137
+ height: root.clientHeight || window.innerHeight,
138
+ filter: (node) => !(node instanceof HTMLElement && node.dataset.tmlsSupportRoot === "true")
139
+ });
121
140
  setShotPreview(URL.createObjectURL(blob));
122
141
  captureShot._blob = blob;
123
142
  } catch {
@@ -159,14 +178,28 @@ function SupportWidget(props) {
159
178
  }, [api]);
160
179
  const openThread = useCallback(async (id) => {
161
180
  setActiveId(id);
162
- setMessages(await api.listMessages(id));
181
+ setActiveConvo(null);
182
+ setMessages([]);
183
+ setShotUrls({});
163
184
  setView("thread");
185
+ const detail = await api.getConversation(id);
186
+ if (!detail) return;
187
+ setActiveConvo(detail);
188
+ setMessages(detail.messages);
189
+ for (const att of detail.attachments) {
190
+ const url = await api.getScreenshot(att.r2_key);
191
+ if (url) setShotUrls((prev) => ({ ...prev, [att.r2_key]: url }));
192
+ }
164
193
  }, [api]);
165
194
  const sendReply = useCallback(async () => {
166
195
  if (!reply.trim() || !activeId) return;
167
196
  await api.reply(activeId, reply.trim());
168
197
  setReply("");
169
- setMessages(await api.listMessages(activeId));
198
+ const detail = await api.getConversation(activeId);
199
+ if (detail) {
200
+ setActiveConvo(detail);
201
+ setMessages(detail.messages);
202
+ }
170
203
  }, [api, reply, activeId]);
171
204
  const chip = (active) => ({
172
205
  padding: "6px 12px",
@@ -178,7 +211,7 @@ function SupportWidget(props) {
178
211
  background: active ? accent : "transparent",
179
212
  color: active ? "#fff" : "#c8c8d0"
180
213
  });
181
- return /* @__PURE__ */ jsxs("div", { style: { position: "fixed", bottom: 20, right: 20, zIndex: 2147483e3, fontFamily: "system-ui, sans-serif" }, children: [
214
+ return /* @__PURE__ */ jsxs("div", { "data-tmls-support-root": "true", style: { position: "fixed", bottom: 20, right: 20, zIndex: 2147483e3, fontFamily: "system-ui, sans-serif" }, children: [
182
215
  open && /* @__PURE__ */ jsxs("div", { style: {
183
216
  position: "absolute",
184
217
  bottom: 60,
@@ -253,6 +286,15 @@ function SupportWidget(props) {
253
286
  ] }, t.id))
254
287
  ] }),
255
288
  view === "thread" && /* @__PURE__ */ jsxs("div", { style: { display: "flex", flexDirection: "column", gap: 10 }, children: [
289
+ activeConvo && /* @__PURE__ */ jsxs("div", { style: { display: "flex", alignItems: "center", gap: 6, flexWrap: "wrap", paddingBottom: 8, borderBottom: "0.5px solid rgba(255,255,255,0.08)" }, children: [
290
+ /* @__PURE__ */ jsxs("span", { style: { fontSize: 13, fontWeight: 600 }, children: [
291
+ "#",
292
+ activeConvo.ticket_number
293
+ ] }),
294
+ /* @__PURE__ */ jsx("span", { style: metaPill("#c8c8d0"), children: activeConvo.type }),
295
+ /* @__PURE__ */ jsx("span", { style: metaPill(SEV_COLOR[activeConvo.severity]), children: activeConvo.severity }),
296
+ /* @__PURE__ */ jsx("span", { style: metaPill(activeConvo.status === "resolved" ? "#30d158" : accent), children: activeConvo.status })
297
+ ] }),
256
298
  messages.map((m) => /* @__PURE__ */ jsx("div", { style: {
257
299
  alignSelf: m.author === "user" ? "flex-end" : "flex-start",
258
300
  maxWidth: "85%",
@@ -262,6 +304,7 @@ function SupportWidget(props) {
262
304
  borderRadius: 12,
263
305
  fontSize: 13
264
306
  }, children: m.body }, m.id)),
307
+ activeConvo?.attachments.map((att) => shotUrls[att.r2_key] ? /* @__PURE__ */ jsx("a", { href: shotUrls[att.r2_key], target: "_blank", rel: "noreferrer", style: { display: "block" }, children: /* @__PURE__ */ jsx("img", { src: shotUrls[att.r2_key], alt: "Attached screenshot", style: { width: "100%", borderRadius: 8, border: "0.5px solid #3a3a42" } }) }, att.id) : /* @__PURE__ */ jsx("div", { style: { fontSize: 11, color: "#8a8a92" }, children: "Loading screenshot\u2026" }, att.id)),
265
308
  /* @__PURE__ */ jsxs("div", { style: { display: "flex", gap: 8 }, children: [
266
309
  /* @__PURE__ */ jsx(
267
310
  "input",
@@ -320,6 +363,18 @@ var primary = (accent) => ({
320
363
  fontWeight: 600,
321
364
  cursor: "pointer"
322
365
  });
366
+ var SEV_COLOR = { blocking: "#ff453a", annoying: "#ff9f0a", minor: "#8e8e93" };
367
+ var metaPill = (color) => ({
368
+ padding: "1px 8px",
369
+ borderRadius: 100,
370
+ fontSize: 10.5,
371
+ fontWeight: 700,
372
+ textTransform: "uppercase",
373
+ letterSpacing: "0.03em",
374
+ color,
375
+ border: `1px solid ${color}55`,
376
+ background: "transparent"
377
+ });
323
378
 
324
379
  // src/index.ts
325
380
  function mountSupportWidget(props) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tmls-ai/support",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "Embeddable Timeless support widget — bottom-right report overlay (type / severity / screenshot) + ticket thread. Auto-captures context, talks to tmls-support-api.",
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",
@@ -8,12 +8,31 @@
8
8
  "main": "./dist/index.js",
9
9
  "module": "./dist/index.js",
10
10
  "types": "./dist/index.d.ts",
11
- "exports": { ".": { "types": "./dist/index.d.ts", "import": "./dist/index.js" } },
12
- "files": ["dist"],
13
- "repository": { "type": "git", "url": "git+https://github.com/tmls-ai/tmls-support-widget.git" },
14
- "keywords": ["support", "widget", "timeless", "tmls"],
15
- "publishConfig": { "access": "public" },
16
- "engines": { "node": ">=18" },
11
+ "exports": {
12
+ ".": {
13
+ "types": "./dist/index.d.ts",
14
+ "import": "./dist/index.js"
15
+ }
16
+ },
17
+ "files": [
18
+ "dist"
19
+ ],
20
+ "repository": {
21
+ "type": "git",
22
+ "url": "git+https://github.com/tmls-ai/tmls-support-widget.git"
23
+ },
24
+ "keywords": [
25
+ "support",
26
+ "widget",
27
+ "timeless",
28
+ "tmls"
29
+ ],
30
+ "publishConfig": {
31
+ "access": "public"
32
+ },
33
+ "engines": {
34
+ "node": ">=18"
35
+ },
17
36
  "scripts": {
18
37
  "build": "tsup src/index.ts --format esm --dts --clean",
19
38
  "typecheck": "tsc --noEmit",