camox 0.56.0 → 0.57.0

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.
@@ -4,13 +4,20 @@ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
4
4
 
5
5
  //#region src/features/studio/components/Squareling.tsx
6
6
  /**
7
- * Camox's agent, drawn from the logo's shapes: its square, the soft corner, and the knockout as
8
- * eyes, looking right like the mark. Its face and motion say what the agent is doing. The
9
- * animations live in `studio.css`.
7
+ * A squareling, drawn from the logo's shapes, with the knockout as eyes, looking right like the
8
+ * mark. Its face and motion say what it is doing. The animations live in `studio.css`.
9
+ *
10
+ * The agent's (the default) is the logo's square with its soft corner, in emerald. Each patroller
11
+ * has its own, one family: the Translator is the logo's wide wedge, in violet, glancing across
12
+ * while it works; the SEO manager is a tall block with the small shape's soft corner, in amber,
13
+ * scanning down while it works.
10
14
  */
11
- function Squareling(t0) {
15
+ function Squareling(props) {
12
16
  const $ = c(18);
13
- const { status, className, ref } = t0;
17
+ const { status, className, ref } = props;
18
+ const variant = props.variant ?? "agent";
19
+ const look = LOOKS[variant];
20
+ const t0 = variant === "agent" ? void 0 : variant;
14
21
  let t1;
15
22
  if ($[0] !== className) {
16
23
  t1 = cn("squareling shrink-0 overflow-visible", className);
@@ -18,98 +25,176 @@ function Squareling(t0) {
18
25
  $[1] = t1;
19
26
  } else t1 = $[1];
20
27
  let t2;
21
- if ($[2] === Symbol.for("react.memo_cache_sentinel")) {
28
+ if ($[2] !== look.body || $[3] !== look.fill) {
22
29
  t2 = /* @__PURE__ */ jsx("path", {
23
- d: "M20 12H80Q88 12 88 20V64Q88 88 64 88H20Q12 88 12 80V20Q12 12 20 12Z",
24
- className: "fill-emerald-500"
30
+ d: look.body,
31
+ className: look.fill
25
32
  });
26
- $[2] = t2;
27
- } else t2 = $[2];
33
+ $[2] = look.body;
34
+ $[3] = look.fill;
35
+ $[4] = t2;
36
+ } else t2 = $[4];
28
37
  let t3;
29
- if ($[3] !== status) {
30
- t3 = (status === "idle" || status === "thinking") && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("rect", {
31
- className: "squareling-eye",
32
- x: "50",
33
- y: "36",
34
- width: "11",
35
- height: "17"
36
- }), /* @__PURE__ */ jsx("rect", {
37
- className: "squareling-eye",
38
- x: "69",
39
- y: "36",
40
- width: "11",
41
- height: "17"
42
- })] });
43
- $[3] = status;
44
- $[4] = t3;
45
- } else t3 = $[4];
38
+ if ($[5] !== look.eyes || $[6] !== props.status || $[7] !== props.variant) {
39
+ t3 = /* @__PURE__ */ jsx("g", {
40
+ className: "squareling-eyes fill-neutral-950",
41
+ children: props.variant === void 0 || props.variant === "agent" ? /* @__PURE__ */ jsx(AgentEyes, {
42
+ eyes: look.eyes,
43
+ status: props.status
44
+ }) : /* @__PURE__ */ jsx(PatrollerEyes, {
45
+ eyes: look.eyes,
46
+ working: props.status === "working"
47
+ })
48
+ });
49
+ $[5] = look.eyes;
50
+ $[6] = props.status;
51
+ $[7] = props.variant;
52
+ $[8] = t3;
53
+ } else t3 = $[8];
46
54
  let t4;
47
- if ($[5] !== status) {
48
- t4 = status === "acting" && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("rect", {
49
- x: "50",
50
- y: "44",
51
- width: "11",
52
- height: "6"
53
- }), /* @__PURE__ */ jsx("rect", {
54
- x: "69",
55
- y: "44",
56
- width: "11",
57
- height: "6"
58
- })] });
59
- $[5] = status;
60
- $[6] = t4;
61
- } else t4 = $[6];
62
- let t5;
63
- if ($[7] !== status) {
64
- t5 = status === "fixing" && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("rect", {
65
- x: "50",
66
- y: "36",
67
- width: "11",
68
- height: "17"
69
- }), /* @__PURE__ */ jsx("rect", {
70
- x: "69",
71
- y: "44",
72
- width: "11",
73
- height: "6"
74
- })] });
75
- $[7] = status;
76
- $[8] = t5;
77
- } else t5 = $[8];
78
- let t6;
79
- if ($[9] !== t3 || $[10] !== t4 || $[11] !== t5) {
80
- t6 = /* @__PURE__ */ jsxs("g", {
55
+ if ($[9] !== t2 || $[10] !== t3) {
56
+ t4 = /* @__PURE__ */ jsxs("g", {
81
57
  className: "squareling-body",
82
- children: [t2, /* @__PURE__ */ jsxs("g", {
83
- className: "squareling-eyes fill-neutral-950",
84
- children: [
85
- t3,
86
- t4,
87
- t5
88
- ]
89
- })]
58
+ children: [t2, t3]
90
59
  });
91
- $[9] = t3;
92
- $[10] = t4;
93
- $[11] = t5;
94
- $[12] = t6;
95
- } else t6 = $[12];
96
- let t7;
97
- if ($[13] !== ref || $[14] !== status || $[15] !== t1 || $[16] !== t6) {
98
- t7 = /* @__PURE__ */ jsx("svg", {
60
+ $[9] = t2;
61
+ $[10] = t3;
62
+ $[11] = t4;
63
+ } else t4 = $[11];
64
+ let t5;
65
+ if ($[12] !== ref || $[13] !== status || $[14] !== t0 || $[15] !== t1 || $[16] !== t4) {
66
+ t5 = /* @__PURE__ */ jsx("svg", {
99
67
  ref,
100
68
  viewBox: "0 0 100 100",
101
69
  "aria-hidden": true,
102
70
  "data-squareling": status,
71
+ "data-squareling-variant": t0,
103
72
  className: t1,
104
- children: t6
73
+ children: t4
105
74
  });
106
- $[13] = ref;
107
- $[14] = status;
75
+ $[12] = ref;
76
+ $[13] = status;
77
+ $[14] = t0;
108
78
  $[15] = t1;
109
- $[16] = t6;
110
- $[17] = t7;
111
- } else t7 = $[17];
112
- return t7;
79
+ $[16] = t4;
80
+ $[17] = t5;
81
+ } else t5 = $[17];
82
+ return t5;
83
+ }
84
+ const LOOKS = {
85
+ agent: {
86
+ body: "M20 12H80Q88 12 88 20V64Q88 88 64 88H20Q12 88 12 80V20Q12 12 20 12Z",
87
+ fill: "fill-emerald-500",
88
+ eyes: {
89
+ x: [50, 69],
90
+ y: 36
91
+ }
92
+ },
93
+ translator: {
94
+ body: "M14 14H86Q92 14 89.6 19.5L67.3 69.6Q60 86 42 86H14Q8 86 8 80V20Q8 14 14 14Z",
95
+ fill: "fill-violet-500",
96
+ eyes: {
97
+ x: [40, 59],
98
+ y: 28
99
+ }
100
+ },
101
+ seoManager: {
102
+ body: "M20 32Q20 8 44 8H74Q80 8 80 14V86Q80 92 74 92H26Q20 92 20 86Z",
103
+ fill: "fill-amber-500",
104
+ eyes: {
105
+ x: [44, 63],
106
+ y: 28
107
+ }
108
+ }
109
+ };
110
+ const EYE_WIDTH = 11;
111
+ const OPEN = 17;
112
+ const NARROWED = 6;
113
+ /** How far down a narrowed eye sits, from the top of the open one. */
114
+ const NARROWED_OFFSET = 8;
115
+ /**
116
+ * One eye, its left edge at `x` and its top at `y` while open: open, or narrowed on the work.
117
+ * Only an eye that blinks carries the class `studio.css` blinks.
118
+ */
119
+ function Eye(t0) {
120
+ const $ = c(5);
121
+ const { x, y, narrowed, blinks } = t0;
122
+ const t1 = blinks ? "squareling-eye" : void 0;
123
+ const t2 = narrowed ? y + NARROWED_OFFSET : y;
124
+ const t3 = narrowed ? NARROWED : OPEN;
125
+ let t4;
126
+ if ($[0] !== t1 || $[1] !== t2 || $[2] !== t3 || $[3] !== x) {
127
+ t4 = /* @__PURE__ */ jsx("rect", {
128
+ className: t1,
129
+ x,
130
+ y: t2,
131
+ width: EYE_WIDTH,
132
+ height: t3
133
+ });
134
+ $[0] = t1;
135
+ $[1] = t2;
136
+ $[2] = t3;
137
+ $[3] = x;
138
+ $[4] = t4;
139
+ } else t4 = $[4];
140
+ return t4;
141
+ }
142
+ /**
143
+ * The agent's eyes: open, blinking, while idle or thinking; both narrowed while acting; the
144
+ * right one narrowed while fixing.
145
+ */
146
+ function AgentEyes(t0) {
147
+ const $ = c(13);
148
+ const { eyes, status } = t0;
149
+ const [left, right] = eyes.x;
150
+ const blinks = status === "idle" || status === "thinking";
151
+ const t1 = status === "acting";
152
+ let t2;
153
+ if ($[0] !== blinks || $[1] !== eyes.y || $[2] !== left || $[3] !== t1) {
154
+ t2 = /* @__PURE__ */ jsx(Eye, {
155
+ x: left,
156
+ y: eyes.y,
157
+ narrowed: t1,
158
+ blinks
159
+ });
160
+ $[0] = blinks;
161
+ $[1] = eyes.y;
162
+ $[2] = left;
163
+ $[3] = t1;
164
+ $[4] = t2;
165
+ } else t2 = $[4];
166
+ const t3 = status === "acting" || status === "fixing";
167
+ let t4;
168
+ if ($[5] !== blinks || $[6] !== eyes.y || $[7] !== right || $[8] !== t3) {
169
+ t4 = /* @__PURE__ */ jsx(Eye, {
170
+ x: right,
171
+ y: eyes.y,
172
+ narrowed: t3,
173
+ blinks
174
+ });
175
+ $[5] = blinks;
176
+ $[6] = eyes.y;
177
+ $[7] = right;
178
+ $[8] = t3;
179
+ $[9] = t4;
180
+ } else t4 = $[9];
181
+ let t5;
182
+ if ($[10] !== t2 || $[11] !== t4) {
183
+ t5 = /* @__PURE__ */ jsxs(Fragment, { children: [t2, t4] });
184
+ $[10] = t2;
185
+ $[11] = t4;
186
+ $[12] = t5;
187
+ } else t5 = $[12];
188
+ return t5;
189
+ }
190
+ /** A patroller's eyes: open while idle, narrowed on the work while working. */
191
+ function PatrollerEyes({ eyes, working }) {
192
+ return eyes.x.map((x) => /* @__PURE__ */ jsx(Eye, {
193
+ x,
194
+ y: eyes.y,
195
+ narrowed: working,
196
+ blinks: true
197
+ }, x));
113
198
  }
114
199
 
115
200
  //#endregion
@@ -1,3 +1,4 @@
1
+ import { cn } from "../../../lib/utils.js";
1
2
  import { Squareling } from "./Squareling.js";
2
3
  import { ThreadMarkdown } from "./ThreadMarkdown.js";
3
4
  import { c } from "react/compiler-runtime";
@@ -16,11 +17,12 @@ import { MessageScroller, MessageScrollerButton, MessageScrollerContent, Message
16
17
  * way to the thread, which hasn't accepted it yet), its face says whether the agent thinks or
17
18
  * acts (uses its tools, or builds its code). While the end of a turn builds the agent's code, or
18
19
  * the agent fixes a build error, it says so in words too. Each editor's message shows its
19
- * sender's avatar, without their name.
20
+ * sender's avatar, without their name. A Live turn handed off to a draft ends on the draft's
21
+ * card, which `onOpenDraft` opens.
20
22
  */
21
23
  function ThreadMessages(t0) {
22
- const $ = c(19);
23
- const { view, sending: t1 } = t0;
24
+ const $ = c(20);
25
+ const { view, sending: t1, onOpenDraft } = t0;
24
26
  const sending = t1 === void 0 ? false : t1;
25
27
  const working = view.running || sending;
26
28
  const empty = view.messages.length === 0 && !working;
@@ -54,58 +56,62 @@ function ThreadMessages(t0) {
54
56
  return t4;
55
57
  }
56
58
  let t2;
57
- if ($[5] !== squareling || $[6] !== view || $[7] !== working) {
59
+ if ($[5] !== onOpenDraft || $[6] !== squareling || $[7] !== view || $[8] !== working) {
58
60
  t2 = view.messages.map((message, index) => /* @__PURE__ */ jsxs(MessageScrollerItem, {
59
61
  messageId: message.id,
60
62
  scrollAnchor: message.from === "editor",
61
63
  className: "flex flex-col gap-4",
62
- children: [/* @__PURE__ */ jsx(ThreadMessageRow, { message }), index === view.messages.length - 1 && /* @__PURE__ */ jsx(AgentPresence, {
64
+ children: [/* @__PURE__ */ jsx(ThreadMessageRow, {
65
+ message,
66
+ onOpenDraft
67
+ }), index === view.messages.length - 1 && /* @__PURE__ */ jsx(AgentPresence, {
63
68
  view,
64
69
  working,
65
70
  squareling
66
71
  })]
67
72
  }, message.id));
68
- $[5] = squareling;
69
- $[6] = view;
70
- $[7] = working;
71
- $[8] = t2;
72
- } else t2 = $[8];
73
+ $[5] = onOpenDraft;
74
+ $[6] = squareling;
75
+ $[7] = view;
76
+ $[8] = working;
77
+ $[9] = t2;
78
+ } else t2 = $[9];
73
79
  let t3;
74
- if ($[9] !== squareling || $[10] !== view || $[11] !== working) {
80
+ if ($[10] !== squareling || $[11] !== view || $[12] !== working) {
75
81
  t3 = view.messages.length === 0 && /* @__PURE__ */ jsx(AgentPresence, {
76
82
  view,
77
83
  working,
78
84
  squareling
79
85
  });
80
- $[9] = squareling;
81
- $[10] = view;
82
- $[11] = working;
83
- $[12] = t3;
84
- } else t3 = $[12];
86
+ $[10] = squareling;
87
+ $[11] = view;
88
+ $[12] = working;
89
+ $[13] = t3;
90
+ } else t3 = $[13];
85
91
  let t4;
86
- if ($[13] !== t2 || $[14] !== t3) {
92
+ if ($[14] !== t2 || $[15] !== t3) {
87
93
  t4 = /* @__PURE__ */ jsx(MessageScrollerViewport, { children: /* @__PURE__ */ jsxs(MessageScrollerContent, {
88
94
  className: "flex flex-col gap-4 px-2 py-3",
89
95
  children: [t2, t3]
90
96
  }) });
91
- $[13] = t2;
92
- $[14] = t3;
93
- $[15] = t4;
94
- } else t4 = $[15];
97
+ $[14] = t2;
98
+ $[15] = t3;
99
+ $[16] = t4;
100
+ } else t4 = $[16];
95
101
  let t5;
96
- if ($[16] === Symbol.for("react.memo_cache_sentinel")) {
102
+ if ($[17] === Symbol.for("react.memo_cache_sentinel")) {
97
103
  t5 = /* @__PURE__ */ jsx(MessageScrollerButton, {});
98
- $[16] = t5;
99
- } else t5 = $[16];
104
+ $[17] = t5;
105
+ } else t5 = $[17];
100
106
  let t6;
101
- if ($[17] !== t4) {
107
+ if ($[18] !== t4) {
102
108
  t6 = /* @__PURE__ */ jsx(MessageScrollerProvider, { children: /* @__PURE__ */ jsxs(MessageScroller, {
103
109
  className: "min-h-0 flex-1",
104
110
  children: [t4, t5]
105
111
  }) });
106
- $[17] = t4;
107
- $[18] = t6;
108
- } else t6 = $[18];
112
+ $[18] = t4;
113
+ $[19] = t6;
114
+ } else t6 = $[19];
109
115
  return t6;
110
116
  }
111
117
  /** How long the greeting's squareling takes to reach the end of the thread, in ms. */
@@ -210,41 +216,54 @@ const STATUS_LINES = {
210
216
  fixing: "Fixing a build error…"
211
217
  };
212
218
  function ThreadMessageRow(t0) {
213
- const $ = c(14);
214
- const { message } = t0;
219
+ const $ = c(17);
220
+ const { message, onOpenDraft } = t0;
221
+ if (message.from === "draft") {
222
+ let t1;
223
+ if ($[0] !== message || $[1] !== onOpenDraft) {
224
+ t1 = /* @__PURE__ */ jsx(Message, { children: /* @__PURE__ */ jsx(MessageContent, { children: /* @__PURE__ */ jsx(DraftCard, {
225
+ message,
226
+ onOpenDraft
227
+ }) }) });
228
+ $[0] = message;
229
+ $[1] = onOpenDraft;
230
+ $[2] = t1;
231
+ } else t1 = $[2];
232
+ return t1;
233
+ }
215
234
  if (message.from === "stopped" || message.from === "failed") {
216
235
  const t1 = message.from === "stopped" ? `Stopped by ${message.sender.name}` : "Camox couldn’t finish this. Send your message again to retry.";
217
236
  let t2;
218
- if ($[0] !== t1) {
237
+ if ($[3] !== t1) {
219
238
  t2 = /* @__PURE__ */ jsx(Message, { children: /* @__PURE__ */ jsx(MessageContent, { children: /* @__PURE__ */ jsx("p", {
220
239
  className: "text-muted-foreground text-sm",
221
240
  children: t1
222
241
  }) }) });
223
- $[0] = t1;
224
- $[1] = t2;
225
- } else t2 = $[1];
242
+ $[3] = t1;
243
+ $[4] = t2;
244
+ } else t2 = $[4];
226
245
  return t2;
227
246
  }
228
247
  if (message.from === "agent") {
229
248
  let t1;
230
- if ($[2] !== message.text) {
249
+ if ($[5] !== message.text) {
231
250
  t1 = /* @__PURE__ */ jsx(Message, { children: /* @__PURE__ */ jsx(MessageContent, { children: /* @__PURE__ */ jsx(ThreadMarkdown, { text: message.text }) }) });
232
- $[2] = message.text;
233
- $[3] = t1;
234
- } else t1 = $[3];
251
+ $[5] = message.text;
252
+ $[6] = t1;
253
+ } else t1 = $[6];
235
254
  return t1;
236
255
  }
237
256
  if (message.from === "camox") {
238
257
  let t1;
239
- if ($[4] === Symbol.for("react.memo_cache_sentinel")) {
258
+ if ($[7] === Symbol.for("react.memo_cache_sentinel")) {
240
259
  t1 = /* @__PURE__ */ jsx(MessageHeader, {
241
260
  className: "text-muted-foreground text-xs",
242
261
  children: "Camox"
243
262
  });
244
- $[4] = t1;
245
- } else t1 = $[4];
263
+ $[7] = t1;
264
+ } else t1 = $[7];
246
265
  let t2;
247
- if ($[5] !== message.text) {
266
+ if ($[8] !== message.text) {
248
267
  t2 = /* @__PURE__ */ jsx(Message, { children: /* @__PURE__ */ jsxs(MessageContent, { children: [t1, /* @__PURE__ */ jsx(Bubble, {
249
268
  variant: "muted",
250
269
  className: "max-w-full",
@@ -253,19 +272,19 @@ function ThreadMessageRow(t0) {
253
272
  children: message.text
254
273
  })
255
274
  })] }) });
256
- $[5] = message.text;
257
- $[6] = t2;
258
- } else t2 = $[6];
275
+ $[8] = message.text;
276
+ $[9] = t2;
277
+ } else t2 = $[9];
259
278
  return t2;
260
279
  }
261
280
  let t1;
262
- if ($[7] !== message.sender) {
281
+ if ($[10] !== message.sender) {
263
282
  t1 = /* @__PURE__ */ jsx(MessageAvatar, { children: /* @__PURE__ */ jsx(SenderAvatar, { sender: message.sender }) });
264
- $[7] = message.sender;
265
- $[8] = t1;
266
- } else t1 = $[8];
283
+ $[10] = message.sender;
284
+ $[11] = t1;
285
+ } else t1 = $[11];
267
286
  let t2;
268
- if ($[9] !== message.text) {
287
+ if ($[12] !== message.text) {
269
288
  t2 = /* @__PURE__ */ jsx(MessageContent, { children: /* @__PURE__ */ jsx(Bubble, {
270
289
  align: "end",
271
290
  variant: "muted",
@@ -275,22 +294,100 @@ function ThreadMessageRow(t0) {
275
294
  children: message.text
276
295
  })
277
296
  }) });
278
- $[9] = message.text;
279
- $[10] = t2;
280
- } else t2 = $[10];
297
+ $[12] = message.text;
298
+ $[13] = t2;
299
+ } else t2 = $[13];
281
300
  let t3;
282
- if ($[11] !== t1 || $[12] !== t2) {
301
+ if ($[14] !== t1 || $[15] !== t2) {
283
302
  t3 = /* @__PURE__ */ jsxs(Message, {
284
303
  align: "end",
285
304
  children: [t1, t2]
286
305
  });
287
- $[11] = t1;
288
- $[12] = t2;
289
- $[13] = t3;
290
- } else t3 = $[13];
306
+ $[14] = t1;
307
+ $[15] = t2;
308
+ $[16] = t3;
309
+ } else t3 = $[16];
291
310
  return t3;
292
311
  }
293
312
  const initials = (name) => name.split(" ").map((part) => part[0]).join("").toUpperCase().slice(0, 2);
313
+ const DRAFT_CARD_CLASSES = "bg-muted/50 flex w-full flex-col items-start gap-0.5 rounded-lg border px-3 py-2 text-left text-sm";
314
+ /**
315
+ * The card of the draft the agent on Live handed the request off to, where its agent does the
316
+ * work: it opens the draft. Once the draft ended, there's nothing to open: it says how it ended.
317
+ */
318
+ function DraftCard(t0) {
319
+ const $ = c(15);
320
+ const { message, onOpenDraft } = t0;
321
+ let t1;
322
+ if ($[0] !== message.name) {
323
+ t1 = /* @__PURE__ */ jsx("span", {
324
+ className: "font-medium",
325
+ children: message.name
326
+ });
327
+ $[0] = message.name;
328
+ $[1] = t1;
329
+ } else t1 = $[1];
330
+ const name = t1;
331
+ if (message.ended) {
332
+ const t2 = message.ended === "published" ? "Published" : "Deleted";
333
+ let t3;
334
+ if ($[2] !== t2) {
335
+ t3 = /* @__PURE__ */ jsx("span", {
336
+ className: "text-muted-foreground text-xs",
337
+ children: t2
338
+ });
339
+ $[2] = t2;
340
+ $[3] = t3;
341
+ } else t3 = $[3];
342
+ let t4;
343
+ if ($[4] !== name || $[5] !== t3) {
344
+ t4 = /* @__PURE__ */ jsxs("div", {
345
+ "data-thread-draft-card": true,
346
+ className: DRAFT_CARD_CLASSES,
347
+ children: [t3, name]
348
+ });
349
+ $[4] = name;
350
+ $[5] = t3;
351
+ $[6] = t4;
352
+ } else t4 = $[6];
353
+ return t4;
354
+ }
355
+ let t2;
356
+ if ($[7] !== message.draftId || $[8] !== onOpenDraft) {
357
+ t2 = () => onOpenDraft?.(message.draftId);
358
+ $[7] = message.draftId;
359
+ $[8] = onOpenDraft;
360
+ $[9] = t2;
361
+ } else t2 = $[9];
362
+ let t3;
363
+ let t4;
364
+ if ($[10] === Symbol.for("react.memo_cache_sentinel")) {
365
+ t3 = cn(DRAFT_CARD_CLASSES, "hover:bg-muted focus-visible:ring-ring transition-colors focus-visible:ring-2 focus-visible:outline-none");
366
+ t4 = /* @__PURE__ */ jsx("span", {
367
+ className: "text-muted-foreground text-xs",
368
+ children: "Started a draft"
369
+ });
370
+ $[10] = t3;
371
+ $[11] = t4;
372
+ } else {
373
+ t3 = $[10];
374
+ t4 = $[11];
375
+ }
376
+ let t5;
377
+ if ($[12] !== name || $[13] !== t2) {
378
+ t5 = /* @__PURE__ */ jsxs("button", {
379
+ type: "button",
380
+ "data-thread-draft-card": true,
381
+ onClick: t2,
382
+ className: t3,
383
+ children: [t4, name]
384
+ });
385
+ $[12] = name;
386
+ $[13] = t2;
387
+ $[14] = t5;
388
+ } else t5 = $[14];
389
+ return t5;
390
+ }
294
391
  function SenderAvatar(t0) {
295
392
  const $ = c(10);
296
393
  const { sender } = t0;
@@ -2,9 +2,13 @@
2
2
  const localeNames = new Intl.DisplayNames(["en"], { type: "language" });
3
3
  /** A locale's name as the studio shows it, "German (de)", or its tag alone when unnamed. */
4
4
  function localeLabel(tag) {
5
+ const name = localeName(tag);
6
+ return name !== tag ? `${name} (${tag})` : tag;
7
+ }
8
+ /** A locale's name in the studio's language, "German", or its tag alone when unnamed. */
9
+ function localeName(tag) {
5
10
  try {
6
- const name = localeNames.of(tag);
7
- return name && name !== tag ? `${name} (${tag})` : tag;
11
+ return localeNames.of(tag) ?? tag;
8
12
  } catch {
9
13
  return tag;
10
14
  }
@@ -115,4 +119,4 @@ function localeOptions(existing) {
115
119
  }
116
120
 
117
121
  //#endregion
118
- export { localeLabel, localeOptions };
122
+ export { localeLabel, localeName, localeOptions };
@@ -0,0 +1,75 @@
1
+ import { localeName } from "./localeOptions.js";
2
+ import { reportLocale } from "@camox/api-contract/patrol";
3
+
4
+ //#region src/features/studio/reportSentence.ts
5
+ const PHRASES = {
6
+ translated: {
7
+ done: "Translated",
8
+ failed: "Couldn't translate",
9
+ locale: "to"
10
+ },
11
+ mergedWithLive: {
12
+ done: "Translated",
13
+ failed: "Couldn't translate",
14
+ locale: "to"
15
+ },
16
+ wroteSeo: {
17
+ done: "Wrote SEO for",
18
+ failed: "Couldn't write SEO for",
19
+ locale: "in"
20
+ },
21
+ describedFile: {
22
+ done: "Described",
23
+ failed: "Couldn't describe",
24
+ locale: "in"
25
+ },
26
+ publishedCatchUp: {
27
+ done: "Published the caught-up translations",
28
+ failed: "Couldn't publish the caught-up translations"
29
+ }
30
+ };
31
+ /** A report as a sentence: "Wrote SEO for Pricing in German", or the text an AI wrote. */
32
+ function reportSentence(summary, outcome) {
33
+ if (summary.kind === "text") return summary.text;
34
+ const phrase = PHRASES[summary.verb];
35
+ const parts = [phrase[outcome]];
36
+ if (summary.subject) parts.push(summary.subject.name);
37
+ if (summary.locale && phrase.locale) parts.push(phrase.locale, localeName(summary.locale));
38
+ return parts.join(" ");
39
+ }
40
+ /**
41
+ * Where clicking a report leads, as its accessible name ends: "show it on the Canvas in German",
42
+ * "open the file on Live". Null for a report that leads nowhere: one without a place.
43
+ */
44
+ function whereReportLeads({ summary, place, live }, inDraft) {
45
+ if (!place) return null;
46
+ const locale = reportLocale(summary);
47
+ const inLocale = locale ? ` in ${localeName(locale)}` : "";
48
+ const onLive = inDraft && live;
49
+ if (place.kind === "file") return `open the file${onLive ? " on Live" : ""}`;
50
+ if (place.kind === "record") return `open it${inLocale}${onLive ? " on Live" : ""}`;
51
+ return `show it on ${onLive ? "Live's" : "the"} Canvas${inLocale}`;
52
+ }
53
+ /** The reports not shown, by locale: "…and 212 more in German, 3 in French". */
54
+ function moreSentence(more) {
55
+ return `…and ${more.map(({ locale, count }, index) => {
56
+ const where = locale === null ? "" : ` in ${localeName(locale)}`;
57
+ return `${count}${index === 0 ? " more" : ""}${where}`;
58
+ }).join(", ")}`;
59
+ }
60
+ const SHORT_UNITS = [
61
+ ["y", 31536e6],
62
+ ["w", 6048e5],
63
+ ["d", 864e5],
64
+ ["h", 36e5],
65
+ ["m", 6e4]
66
+ ];
67
+ /** How long ago a moment was, briefly: "now", "2m", "1h". */
68
+ function shortTimeAgo(at, now = Date.now()) {
69
+ const elapsed = now - at;
70
+ for (const [unit, size] of SHORT_UNITS) if (elapsed >= size) return `${Math.floor(elapsed / size)}${unit}`;
71
+ return "now";
72
+ }
73
+
74
+ //#endregion
75
+ export { moreSentence, reportSentence, shortTimeAgo, whereReportLeads };