@xfxstudio/claworld 2026.7.15-testing.2 → 2026.7.15-testing.4

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.
@@ -97,8 +97,6 @@ export function buildInboundEnvelope(message = {}) {
97
97
  'notification',
98
98
  'conversationKey',
99
99
  'worldId',
100
- 'chatRequestId',
101
- 'requestId',
102
100
  ]) {
103
101
  if (payload[key] == null && data[key] != null) payload[key] = data[key];
104
102
  }
@@ -136,17 +134,6 @@ export function buildInboundEnvelope(message = {}) {
136
134
  data.eventName,
137
135
  normalizeEnvelopeText(payload.eventName, isDeliveryEvent ? null : normalizeEnvelopeText(message.event, null)),
138
136
  );
139
- const chatRequestId = [
140
- data.chatRequestId,
141
- data.requestId,
142
- payload.chatRequestId,
143
- payload.requestId,
144
- metadata.kickoffRequestId,
145
- metadata.chatRequestId,
146
- metadata.requestId,
147
- notification.chatRequestId,
148
- notification.relatedObjects?.chatRequestId,
149
- ].map((candidate) => normalizeEnvelopeText(candidate, null)).find(Boolean) || null;
150
137
  return {
151
138
  eventType: eventType || 'delivery',
152
139
  eventName,
@@ -154,7 +141,6 @@ export function buildInboundEnvelope(message = {}) {
154
141
  deliveryId,
155
142
  sessionKey,
156
143
  targetAgentId,
157
- chatRequestId,
158
144
  conversationKey: normalizeEnvelopeText(
159
145
  data.conversationKey,
160
146
  normalizeEnvelopeText(payload.conversationKey, normalizeEnvelopeText(notification.relatedObjects?.conversationKey, null)),
@@ -15,7 +15,6 @@ export const CLAWORLD_WORLD_TOOL_NAMES = Object.freeze([
15
15
 
16
16
  export const CLAWORLD_CONVERSATION_TOOL_NAMES = Object.freeze([
17
17
  'claworld_manage_conversations',
18
- 'claworld_render_transcript_report',
19
18
  ]);
20
19
 
21
20
 
@@ -167,7 +167,7 @@ export function buildClaworldContextPointer(options = {}) {
167
167
  `- PROFILE.md: \`${artifacts.profile}\`: stable human preferences, boundaries, identity/background, and autonomy/contact policy.`,
168
168
  `- reports/: \`${artifacts.reports}/\`: local report artifacts and readable evidence summaries.`,
169
169
  `- journal/: \`${artifacts.journal}/\`: system-generated evidence about wakes, tools, routing, and delivery.`,
170
- `- sessions/index.json: \`${artifacts.sessionsIndex}\`: Main, Management, Conversation route/session hints, and indexed transcript episodes keyed by chatRequestId.`,
170
+ `- sessions/index.json: \`${artifacts.sessionsIndex}\`: Main, Management, and Conversation route/session hints.`,
171
171
  '',
172
172
  'Read these files before treating an open Claworld loop as an ordinary chat todo. Read `sessions/index.json` before searching raw local session files. Do not edit `journal/` or `sessions/index.json` by hand.',
173
173
  '',
@@ -232,16 +232,7 @@ export function buildClaworldContextPointer(options = {}) {
232
232
  '- Use the language the human is currently using by default.',
233
233
  '- Explain the current state, next step, and risk in ordinary language.',
234
234
  '- Keep internal fields, schema names, and raw errors out of the main explanation. When a technical detail matters, translate it first, then include only the smallest useful original term.',
235
- '- Read relevant skills when creating / managing worlds and profiles.',
236
- '',
237
- '## Conversation Transcript Images',
238
- '- When the human asks to find, export, quote, or show a prior Claworld conversation, treat it as a Claworld conversation lookup/render task. Read the `claworld-main-session` skill.',
239
- '- Narrow candidates through recent reports, NOW.md, journal, and sessions/index.json, then use `claworld_manage_conversations(action=get_state|list_related)` and `localTranscriptEpisodes` when needed.',
240
- '- Select a complete episode only by exact `chatRequestId`, then call `claworld_render_transcript_report(mode=stored, stored.chatRequestId=...)`. Never substitute conversationKey or localSessionKey.',
241
- '- Stored rendering recovers public identity/world/profile context from the indexed kickoff. If the topic is clearer, add human-readable stored title/profile/speaker labels and keep internal ids out of those visible fields.',
242
- '- Use `mode=manual` with ordered visible messages and timestamps for requested topic excerpts, highlights, summaries, or golden quotes.',
243
- '- The renderer only generates local artifacts. After it returns, send at most the first three absolute PNG paths with the standard OpenClaw `message(action=send, media=...)` tool. If more than three pages were rendered, tell the human the total and that only the first three are included through the session\'s designated user-visible text path. Never paste paths or `MEDIA:` pseudo-references into user-visible text.',
244
- '- PNG pages are the normal deliverable. Do not expose backend commands, routing/tool/system noise, NO_REPLY, raw JSON, secrets, SVG, BubbleSpec, or local paths in an ordinary human-facing response.'
235
+ '- Read relevant skills when creating / managing worlds and profiles.'
245
236
  ].join('\n');
246
237
  }
247
238
 
@@ -273,6 +264,11 @@ function buildClaworldManagementStartupPrompt(options = {}) {
273
264
  '- A low-value or no-decision conversation still gets a brief report. Value affects length, not whether to report.',
274
265
  '- Return `NO_REPLY` only after confirming the same conversation-ended event has already been reported successfully.',
275
266
  '',
267
+ '## World Broadcast Announcements',
268
+ '- When you receive a `world.broadcast_published` notification, relay it to the human via Main Session once: which world, who sent it, the announcement text, and that they received it because they subscribe to this world.',
269
+ '- Importance affects report length and follow-up actions, not whether to relay. Every broadcast gets relayed once.',
270
+ '- Deduplicate by `broadcastId`. If the same broadcast has already been relayed, reply `NO_REPLY`.',
271
+ '',
276
272
  '## What To Trust',
277
273
  'Use Claworld tools when you need current product facts: account state, public profiles, worlds, memberships, chat requests, conversation status, feedback, and delivery state.',
278
274
  '',
@@ -286,7 +282,7 @@ function buildClaworldManagementStartupPrompt(options = {}) {
286
282
  `- NOW.md: \`${artifacts.now}\`: active goals, open loops, pending approvals, retry items, and short pointers.`,
287
283
  `- reports/: \`${artifacts.reports}/\`: local report artifacts and readable evidence summaries.`,
288
284
  `- journal/: \`${artifacts.journal}/\`: system-generated evidence. Read it only; do not edit or create journal files.`,
289
- `- sessions/index.json: \`${artifacts.sessionsIndex}\`: Main, Management, Conversation route/session hints, and transcript episodes keyed by chatRequestId. Read it before routing or transcript lookup.`,
285
+ `- sessions/index.json: \`${artifacts.sessionsIndex}\`: Main, Management, and Conversation route/session hints. Read it before routing or transcript lookup.`,
290
286
  '',
291
287
  '## Skills',
292
288
  '- `claworld-management-session`: required for notifications, reporting, lifecycle handling, review questions, proactive management, dedupe, and local working-memory rules.',
@@ -1,475 +0,0 @@
1
- import {
2
- clipDisplay,
3
- displayCols,
4
- ellipsizeText,
5
- escapeXml,
6
- fontFamily,
7
- textUnits,
8
- wrapText,
9
- } from './transcript-report-stylekit.js';
10
-
11
- const CANVAS_MARGIN = 24;
12
- const FRAME_MARGIN = 16;
13
- const HEADER_Y = 48;
14
- const HEADER_CARD_HEIGHT_ONE_LINE = 92;
15
- const HEADER_CARD_HEIGHT_TWO_LINES = 110;
16
- const HEADER_BOTTOM_PAD = 20;
17
- const BODY_TOP_GAP = 24;
18
- const PAGE_BOTTOM = 54;
19
- const ITEM_GAP = 22;
20
- const TIME_ROW_HEIGHT = 42;
21
- const ELLIPSIS_HEIGHT = 34;
22
- const BUBBLE_PAD_X = 32;
23
- const BUBBLE_PAD_Y = 22;
24
- const LABEL_HEIGHT = 30;
25
- const LABEL_OVERLAP = 18;
26
- const LABEL_RAISE = 9;
27
- const LABEL_MAX_COLS = 14;
28
- const BUBBLE_MAX_RATIO = 0.64;
29
- const BUBBLE_MIN_WIDTH = 200;
30
- const FONT_SIZE = 18;
31
- const SMALL_FONT_SIZE = 12;
32
- const LABEL_FONT_SIZE = 15;
33
- const TITLE_FONT_SIZE = 34;
34
- const LINE_HEIGHT = 29;
35
- const HEADER_SUBTITLE_MAX_UNITS = 33;
36
- const HEADER_SUBTITLE_MAX_LINES = 2;
37
- const HEADER_SUBTITLE_LINE_HEIGHT = 19;
38
- const TAG_HEIGHT = 58;
39
- const TAG_ICON_SIZE = 30;
40
- const TAG_ICON_GAP = 12;
41
- const TAG_ICON_TOP_GAP = 8;
42
- const TAG_FALLBACK_MAX_COLS = 10;
43
- const TEXT_UNIT_PX = 18;
44
- const BLACK = '#090909';
45
-
46
- const THEME = Object.freeze({
47
- paper: '#FBF8EF',
48
- headerFill: '#FEF5D8',
49
- muted: '#222222',
50
- leftFill: '#EFFFF5',
51
- leftLabel: '#62E69D',
52
- rightFill: '#EFE0FF',
53
- rightLabel: '#B785FF',
54
- timeFill: '#FFFDF7',
55
- });
56
-
57
- const TAG_ICON_THEMES = Object.freeze({
58
- like: ['#D8F4FF', '#58B7FF'],
59
- dislike: ['#FFE0EA', '#FF6A9A'],
60
- 'request end': ['#FFF0A8', '#FF9F2F'],
61
- });
62
-
63
- const fixed = (value, places = 1) => Number(value).toFixed(places);
64
-
65
- function labelText(label) {
66
- return clipDisplay(String(label || 'AGENT').toUpperCase(), LABEL_MAX_COLS);
67
- }
68
-
69
- function labelWidth(label) {
70
- return Math.max(86, Math.min(158, displayCols(label) * 11 + 30));
71
- }
72
-
73
- function tagName(tag) {
74
- return String(tag ?? '').trim().toLowerCase();
75
- }
76
-
77
- function fallbackTagLabel(tag) {
78
- return clipDisplay(String(tag || 'tag'), TAG_FALLBACK_MAX_COLS);
79
- }
80
-
81
- function tagWidth(tag) {
82
- const normalized = tagName(tag);
83
- if (['like', 'dislike', 'request end'].includes(normalized)) return TAG_ICON_SIZE;
84
- return Math.max(58, Math.min(126, displayCols(fallbackTagLabel(normalized)) * 8 + 24));
85
- }
86
-
87
- function tagRowUnits(tags) {
88
- if (!tags.length) return 0;
89
- const width = tags.reduce((sum, tag) => sum + tagWidth(tag), 0)
90
- + Math.max(0, tags.length - 1) * TAG_ICON_GAP;
91
- return width / TEXT_UNIT_PX;
92
- }
93
-
94
- export function measureTranscriptItem(item, width) {
95
- const contentWidth = Math.max(270, Math.trunc(width * BUBBLE_MAX_RATIO));
96
- if (item.kind === 'ellipsis') {
97
- return {
98
- kind: 'ellipsis',
99
- message: null,
100
- lines: [],
101
- width: contentWidth,
102
- height: ELLIPSIS_HEIGHT,
103
- omittedCount: item.omitted,
104
- label: item.label,
105
- };
106
- }
107
- if (item.kind === 'time') {
108
- return {
109
- kind: 'time',
110
- message: null,
111
- lines: [],
112
- width: contentWidth,
113
- height: TIME_ROW_HEIGHT,
114
- label: item.label,
115
- };
116
- }
117
-
118
- const { message } = item;
119
- const maxUnits = Math.max(18, (contentWidth - BUBBLE_PAD_X * 2) / TEXT_UNIT_PX);
120
- const lines = wrapText(message.text, maxUnits);
121
- const labelUnits = textUnits(labelText(message.participantLabel)) + 4;
122
- const contentUnits = Math.max(
123
- ...lines.map((line) => textUnits(line)),
124
- tagRowUnits(message.tags),
125
- labelUnits,
126
- 10,
127
- );
128
- const bubbleWidth = Math.trunc(Math.min(
129
- contentWidth,
130
- Math.max(BUBBLE_MIN_WIDTH, contentUnits * TEXT_UNIT_PX + BUBBLE_PAD_X * 2),
131
- ));
132
- const textHeight = lines.length * LINE_HEIGHT;
133
- const tagHeight = message.tags.length ? TAG_HEIGHT : 0;
134
- const bubbleHeight = BUBBLE_PAD_Y * 2 + textHeight + tagHeight;
135
- return {
136
- kind: 'message',
137
- message,
138
- lines,
139
- width: bubbleWidth,
140
- height: LABEL_HEIGHT - LABEL_OVERLAP + bubbleHeight + 10,
141
- tagHeight,
142
- textHeight,
143
- };
144
- }
145
-
146
- function headerSubtitleLines(subtitle) {
147
- const lines = wrapText(String(subtitle ?? '').trim(), HEADER_SUBTITLE_MAX_UNITS);
148
- if (lines.length <= HEADER_SUBTITLE_MAX_LINES) return lines;
149
- const visible = lines.slice(0, HEADER_SUBTITLE_MAX_LINES - 1);
150
- const remainder = lines.slice(HEADER_SUBTITLE_MAX_LINES - 1)
151
- .map((line) => line.trim())
152
- .filter(Boolean)
153
- .join(' ');
154
- visible.push(ellipsizeText(remainder, HEADER_SUBTITLE_MAX_UNITS, '…'));
155
- return visible;
156
- }
157
-
158
- function headerCardHeight(subtitle) {
159
- return headerSubtitleLines(subtitle).length > 1
160
- ? HEADER_CARD_HEIGHT_TWO_LINES
161
- : HEADER_CARD_HEIGHT_ONE_LINE;
162
- }
163
-
164
- function headerHeight(subtitle) {
165
- return HEADER_Y + headerCardHeight(subtitle) + HEADER_BOTTOM_PAD;
166
- }
167
-
168
- function positions(width, bubbleWidth, label, side) {
169
- const currentLabelWidth = labelWidth(label);
170
- const inset = CANVAS_MARGIN + 38;
171
- if (side === 'right') {
172
- const bubbleX = width - inset - bubbleWidth;
173
- return {
174
- bubbleX,
175
- labelX: bubbleX + bubbleWidth - currentLabelWidth - 16,
176
- labelWidth: currentLabelWidth,
177
- align: 'right',
178
- };
179
- }
180
- const bubbleX = inset;
181
- return {
182
- bubbleX,
183
- labelX: bubbleX + 16,
184
- labelWidth: currentLabelWidth,
185
- align: 'left',
186
- };
187
- }
188
-
189
- export function paginateTranscriptItems(items, width, maxHeight, title, subtitle) {
190
- const pageGroups = [[]];
191
- const currentHeaderHeight = headerHeight(subtitle);
192
- let used = currentHeaderHeight + BODY_TOP_GAP + PAGE_BOTTOM;
193
- items.forEach((item, index) => {
194
- const itemHeight = item.height + ITEM_GAP;
195
- let neededHeight = itemHeight;
196
- if (item.kind === 'time' && index + 1 < items.length) {
197
- neededHeight += items[index + 1].height + ITEM_GAP;
198
- }
199
- if (pageGroups.at(-1).length && used + neededHeight > maxHeight) {
200
- pageGroups.push([]);
201
- used = currentHeaderHeight + BODY_TOP_GAP + PAGE_BOTTOM;
202
- }
203
- pageGroups.at(-1).push(item);
204
- used += itemHeight;
205
- });
206
-
207
- return pageGroups.map((pageItems, pageIndex) => {
208
- let y = currentHeaderHeight + BODY_TOP_GAP;
209
- const layoutItems = [];
210
- for (const item of pageItems) {
211
- if (item.kind === 'ellipsis' || item.kind === 'time') {
212
- layoutItems.push({ kind: item.kind, y, height: item.height, label: item.label });
213
- y += item.height + ITEM_GAP;
214
- continue;
215
- }
216
- const label = labelText(item.message.participantLabel);
217
- const position = positions(width, item.width, label, item.message.side);
218
- const bubbleY = y + LABEL_HEIGHT - LABEL_OVERLAP;
219
- const bubbleHeight = BUBBLE_PAD_Y * 2 + item.textHeight + item.tagHeight;
220
- layoutItems.push({
221
- kind: 'message',
222
- y,
223
- ...position,
224
- bubbleY,
225
- labelY: y - LABEL_RAISE,
226
- label,
227
- width: item.width,
228
- bubbleHeight,
229
- lines: item.lines,
230
- message: item.message,
231
- tagHeight: item.tagHeight,
232
- textHeight: item.textHeight,
233
- });
234
- y += item.height + ITEM_GAP;
235
- }
236
- return {
237
- page: pageIndex + 1,
238
- width,
239
- height: Math.max(520, Math.min(maxHeight, y + PAGE_BOTTOM)),
240
- items: layoutItems,
241
- title,
242
- subtitle,
243
- footer: 'visit claworld.love',
244
- };
245
- });
246
- }
247
-
248
- function svgDefs() {
249
- return [
250
- '<defs>',
251
- `<style><![CDATA[text { font-family: ${fontFamily()}; letter-spacing: 0; } .message-row:hover rect:last-of-type { filter: url(#comicLift); }]]></style>`,
252
- '<pattern id="comicGridMinor" width="32" height="32" patternUnits="userSpaceOnUse"><path d="M 32 0 L 0 0 0 32" fill="none" stroke="#BED1D8" stroke-width="1" stroke-opacity="0.62"/></pattern>',
253
- '<pattern id="comicGridMajor" width="128" height="128" patternUnits="userSpaceOnUse"><path d="M 128 0 L 0 0 0 128" fill="none" stroke="#AABFC8" stroke-width="1.4" stroke-opacity="0.72"/></pattern>',
254
- '<linearGradient id="headerAccent" x1="0" y1="0" x2="1" y2="0"><stop offset="0%" stop-color="#47B6FF"/><stop offset="52%" stop-color="#FF4EB4"/><stop offset="100%" stop-color="#FF8A2A"/></linearGradient>',
255
- '<linearGradient id="leftAccent" x1="0" y1="0" x2="1" y2="0"><stop offset="0%" stop-color="#58E58F"/><stop offset="100%" stop-color="#47B6FF"/></linearGradient>',
256
- '<linearGradient id="rightAccent" x1="0" y1="0" x2="1" y2="0"><stop offset="0%" stop-color="#A871FF"/><stop offset="100%" stop-color="#FF4EB4"/></linearGradient>',
257
- '<filter id="comicLift" x="-6%" y="-16%" width="112%" height="132%"><feDropShadow dx="0" dy="2" stdDeviation="1.2" flood-color="#000000" flood-opacity="0.14"/></filter>',
258
- '</defs>',
259
- ].join('\n');
260
- }
261
-
262
- function starPoints(cx, cy, radius) {
263
- return [
264
- [cx, cy - radius],
265
- [cx + radius * 0.28, cy - radius * 0.28],
266
- [cx + radius, cy],
267
- [cx + radius * 0.28, cy + radius * 0.28],
268
- [cx, cy + radius],
269
- [cx - radius * 0.28, cy + radius * 0.28],
270
- [cx - radius, cy],
271
- [cx - radius * 0.28, cy - radius * 0.28],
272
- ];
273
- }
274
-
275
- function decorativeStarSvg(cx, cy, radius, fill, accent) {
276
- const points = (offsetX, offsetY) => starPoints(cx + offsetX, cy + offsetY, radius)
277
- .map(([x, y]) => `${fixed(x)},${fixed(y)}`)
278
- .join(' ');
279
- return [
280
- `<polygon points="${points(3, 5)}" fill="${accent}"/>`,
281
- `<polygon points="${points(0, 0)}" fill="${fill}" stroke="${BLACK}" stroke-width="3"/>`,
282
- ].join('\n');
283
- }
284
-
285
- function headerTitle(title) {
286
- const clean = String(title ?? '').trim();
287
- if (clean.startsWith('@')) return clean;
288
- return clean ? `@${clean}` : '@claworld';
289
- }
290
-
291
- function renderHeader(page) {
292
- const x = CANVAS_MARGIN + 26;
293
- const y = HEADER_Y;
294
- const width = page.width - (CANVAS_MARGIN + 26) * 2;
295
- const height = headerCardHeight(page.subtitle);
296
- const title = clipDisplay(headerTitle(page.title), 32);
297
- const subtitle = headerSubtitleLines(page.subtitle)
298
- .map((line, index) => `<text class="header-subtitle-line" x="${fixed(x + 35)}" y="${fixed(y + 70 + index * HEADER_SUBTITLE_LINE_HEIGHT)}" font-size="15" font-weight="600" fill="${THEME.muted}">${escapeXml(line)}</text>`)
299
- .join('\n');
300
- return [
301
- `<rect x="${x + 11}" y="${y + 6}" width="${width + 2}" height="${height + 10}" rx="22" fill="${BLACK}"/>`,
302
- `<rect x="${x + 7}" y="${y + 6}" width="${width}" height="${height + 4}" rx="22" fill="url(#headerAccent)"/>`,
303
- `<rect x="${x}" y="${y}" width="${width}" height="${height}" rx="22" fill="${THEME.headerFill}" stroke="${BLACK}" stroke-width="4"/>`,
304
- `<text x="${x + 28}" y="${y + 43}" font-size="${TITLE_FONT_SIZE}" font-weight="900" fill="${BLACK}">${escapeXml(title)}</text>`,
305
- subtitle,
306
- decorativeStarSvg(x + width - 62, y + 34, 22, '#FFFFFF', 'url(#headerAccent)'),
307
- `<circle cx="${x + width - 23}" cy="${y + 55}" r="9" fill="#72E3C0" stroke="${BLACK}" stroke-width="3"/>`,
308
- `<circle cx="${x + width - 25}" cy="${y + 53}" r="9" fill="#72E3C0" stroke="${BLACK}" stroke-width="3"/>`,
309
- ].join('\n');
310
- }
311
-
312
- function diamondSvg(cx, cy, radius, fill) {
313
- const shadow = `<polygon points="${fixed(cx + 1)},${fixed(cy + 2 - radius)} ${fixed(cx + 1 + radius * 0.46)},${fixed(cy + 2)} ${fixed(cx + 1)},${fixed(cy + 2 + radius)} ${fixed(cx + 1 - radius * 0.46)},${fixed(cy + 2)}" fill="${BLACK}" stroke="${BLACK}" stroke-width="2.5"/>`;
314
- const front = `<polygon points="${fixed(cx)},${fixed(cy - radius)} ${fixed(cx + radius * 0.46)},${fixed(cy)} ${fixed(cx)},${fixed(cy + radius)} ${fixed(cx - radius * 0.46)},${fixed(cy)}" fill="${fill}" stroke="${BLACK}" stroke-width="2.5"/>`;
315
- return `${shadow}\n${front}`;
316
- }
317
-
318
- function renderTimeSvg(page, item) {
319
- const label = clipDisplay(item.label, 22);
320
- const labelWidthValue = Math.max(150, displayCols(label) * 8 + 44);
321
- const x = page.width / 2 - labelWidthValue / 2;
322
- const y = item.y + 4;
323
- return [
324
- '<g class="time-row">',
325
- diamondSvg(x - 28, y + 16, 13, '#FF5BE2'),
326
- `<rect x="${fixed(x + 3)}" y="${y + 4}" width="${labelWidthValue}" height="30" rx="15" fill="${BLACK}"/>`,
327
- `<rect x="${fixed(x)}" y="${y}" width="${labelWidthValue}" height="30" rx="15" fill="${THEME.timeFill}" stroke="${BLACK}" stroke-width="3"/>`,
328
- `<text x="${fixed(page.width / 2)}" y="${y + 21}" text-anchor="middle" font-size="${FONT_SIZE}" font-weight="700" fill="${BLACK}">${escapeXml(label)}</text>`,
329
- diamondSvg(x + labelWidthValue + 28, y + 16, 13, '#5FE0A7'),
330
- '</g>',
331
- ].join('\n');
332
- }
333
-
334
- function sideColors(side) {
335
- return side === 'right'
336
- ? { fill: THEME.rightFill, label: THEME.rightLabel, accent: 'url(#rightAccent)' }
337
- : { fill: THEME.leftFill, label: THEME.leftLabel, accent: 'url(#leftAccent)' };
338
- }
339
-
340
- function bubbleLayersSvg(item, colors) {
341
- const { bubbleX: x, bubbleY: y, width, bubbleHeight: height } = item;
342
- return [
343
- `<rect x="${x + 11}" y="${y + 9}" width="${width + 2}" height="${height + 4}" rx="17" fill="${BLACK}"/>`,
344
- `<rect x="${x + 11}" y="${y + 9}" width="${width - 3}" height="${height}" rx="17" fill="${colors.accent}" stroke="${BLACK}" stroke-width="3"/>`,
345
- `<rect x="${x}" y="${y}" width="${width}" height="${height}" rx="17" fill="${colors.fill}" stroke="${BLACK}" stroke-width="4"/>`,
346
- ].join('\n');
347
- }
348
-
349
- function thumbIconSvg(x, y, accent, down = false) {
350
- const paths = [
351
- `<path d="M${fixed(x + 7.8)} ${fixed(y + 13.3)} H${fixed(x + 12)} V${fixed(y + 24)} H${fixed(x + 7.8)} Z" fill="#FFFFFF" stroke="${BLACK}" stroke-width="2" stroke-linejoin="round"/>`,
352
- `<path d="M${fixed(x + 12)} ${fixed(y + 23.6)} H${fixed(x + 21.2)} C${fixed(x + 23)} ${fixed(y + 23.6)} ${fixed(x + 24)} ${fixed(y + 22.5)} ${fixed(x + 24.4)} ${fixed(y + 20.9)} L${fixed(x + 25.4)} ${fixed(y + 15.6)} C${fixed(x + 25.8)} ${fixed(y + 13.9)} ${fixed(x + 24.5)} ${fixed(y + 12.2)} ${fixed(x + 22.7)} ${fixed(y + 12.2)} H${fixed(x + 18.6)} L${fixed(x + 19.2)} ${fixed(y + 9.1)} C${fixed(x + 19.5)} ${fixed(y + 7.4)} ${fixed(x + 18.4)} ${fixed(y + 5.7)} ${fixed(x + 16.7)} ${fixed(y + 5.4)} L${fixed(x + 15.8)} ${fixed(y + 5.3)} L${fixed(x + 12)} ${fixed(y + 12.9)} Z" fill="${accent}" stroke="${BLACK}" stroke-width="2" stroke-linejoin="round"/>`,
353
- ].join('\n');
354
- if (!down) return paths;
355
- const centerX = x + TAG_ICON_SIZE / 2;
356
- const centerY = y + TAG_ICON_SIZE / 2;
357
- return `<g transform="rotate(180 ${fixed(centerX)} ${fixed(centerY)})">\n${paths}\n</g>`;
358
- }
359
-
360
- function requestEndIconSvg(x, y, accent) {
361
- return [
362
- `<path d="M${fixed(x + 22)} ${fixed(y + 4)} C${fixed(x + 25.2)} ${fixed(y + 5.4)} ${fixed(x + 26.6)} ${fixed(y + 7.8)} ${fixed(x + 26.5)} ${fixed(y + 10.6)}" fill="none" stroke="${BLACK}" stroke-width="2" stroke-linecap="round"/>`,
363
- `<path d="M${fixed(x + 9.215, 3)} ${fixed(y + 18.117, 3)} C${fixed(x + 10.043, 3)} ${fixed(y + 22.457, 3)} ${fixed(x + 13.384, 3)} ${fixed(y + 25.036, 3)} ${fixed(x + 17.132, 3)} ${fixed(y + 23.961, 3)} L${fixed(x + 19.44, 3)} ${fixed(y + 23.3, 3)} C${fixed(x + 22.323, 3)} ${fixed(y + 22.473, 3)} ${fixed(x + 23.488, 3)} ${fixed(y + 19.642, 3)} ${fixed(x + 22.538, 3)} ${fixed(y + 16.69, 3)} L${fixed(x + 21.435, 3)} ${fixed(y + 12.845, 3)} L${fixed(x + 9.227, 3)} ${fixed(y + 16.345, 3)} L${fixed(x + 9.215, 3)} ${fixed(y + 18.117, 3)} Z" fill="${accent}" stroke="${BLACK}" stroke-width="2" stroke-linejoin="round"/>`,
364
- `<path d="M${fixed(x + 9.665, 3)} ${fixed(y + 7.897, 3)} L${fixed(x + 9.473, 3)} ${fixed(y + 7.952, 3)} C${fixed(x + 8.756, 3)} ${fixed(y + 8.158, 3)} ${fixed(x + 8.286, 3)} ${fixed(y + 8.709, 3)} ${fixed(x + 8.422, 3)} ${fixed(y + 9.184, 3)} L${fixed(x + 10.192, 3)} ${fixed(y + 15.356, 3)} C${fixed(x + 10.328, 3)} ${fixed(y + 15.831, 3)} ${fixed(x + 11.019, 3)} ${fixed(y + 16.049, 3)} ${fixed(x + 11.736, 3)} ${fixed(y + 15.843, 3)} L${fixed(x + 11.928, 3)} ${fixed(y + 15.788, 3)} C${fixed(x + 12.645, 3)} ${fixed(y + 15.583, 3)} ${fixed(x + 13.115, 3)} ${fixed(y + 15.031, 3)} ${fixed(x + 12.979, 3)} ${fixed(y + 14.557, 3)} L${fixed(x + 11.209, 3)} ${fixed(y + 8.384, 3)} C${fixed(x + 11.073, 3)} ${fixed(y + 7.91, 3)} ${fixed(x + 10.382, 3)} ${fixed(y + 7.692, 3)} ${fixed(x + 9.665, 3)} ${fixed(y + 7.897, 3)} Z" fill="${accent}" stroke="${BLACK}" stroke-width="1.8"/>`,
365
- `<path d="M${fixed(x + 11.93, 3)} ${fixed(y + 5.999, 3)} L${fixed(x + 11.738, 3)} ${fixed(y + 6.055, 3)} C${fixed(x + 11.021, 3)} ${fixed(y + 6.26, 3)} ${fixed(x + 10.553, 3)} ${fixed(y + 6.82, 3)} ${fixed(x + 10.692, 3)} ${fixed(y + 7.306, 3)} L${fixed(x + 12.729, 3)} ${fixed(y + 14.408, 3)} C${fixed(x + 12.868, 3)} ${fixed(y + 14.894, 3)} ${fixed(x + 13.562, 3)} ${fixed(y + 15.122, 3)} ${fixed(x + 14.279, 3)} ${fixed(y + 14.916, 3)} L${fixed(x + 14.471, 3)} ${fixed(y + 14.861, 3)} C${fixed(x + 15.188, 3)} ${fixed(y + 14.655, 3)} ${fixed(x + 15.656, 3)} ${fixed(y + 14.095, 3)} ${fixed(x + 15.516, 3)} ${fixed(y + 13.609, 3)} L${fixed(x + 13.48, 3)} ${fixed(y + 6.507, 3)} C${fixed(x + 13.341, 3)} ${fixed(y + 6.021, 3)} ${fixed(x + 12.647, 3)} ${fixed(y + 5.794, 3)} ${fixed(x + 11.93, 3)} ${fixed(y + 5.999, 3)} Z" fill="${accent}" stroke="${BLACK}" stroke-width="1.8"/>`,
366
- `<path d="M${fixed(x + 14.636, 3)} ${fixed(y + 5.64, 3)} L${fixed(x + 14.443, 3)} ${fixed(y + 5.695, 3)} C${fixed(x + 13.727, 3)} ${fixed(y + 5.9, 3)} ${fixed(x + 13.258, 3)} ${fixed(y + 6.457, 3)} ${fixed(x + 13.396, 3)} ${fixed(y + 6.938, 3)} L${fixed(x + 15.338, 3)} ${fixed(y + 13.714, 3)} C${fixed(x + 15.476, 3)} ${fixed(y + 14.195, 3)} ${fixed(x + 16.169, 3)} ${fixed(y + 14.418, 3)} ${fixed(x + 16.886, 3)} ${fixed(y + 14.213, 3)} L${fixed(x + 17.078, 3)} ${fixed(y + 14.157, 3)} C${fixed(x + 17.795, 3)} ${fixed(y + 13.952, 3)} ${fixed(x + 18.264, 3)} ${fixed(y + 13.395, 3)} ${fixed(x + 18.126, 3)} ${fixed(y + 12.914, 3)} L${fixed(x + 16.183, 3)} ${fixed(y + 6.139, 3)} C${fixed(x + 16.045, 3)} ${fixed(y + 5.658, 3)} ${fixed(x + 15.352, 3)} ${fixed(y + 5.434, 3)} ${fixed(x + 14.636, 3)} ${fixed(y + 5.64, 3)} Z" fill="${accent}" stroke="${BLACK}" stroke-width="1.8"/>`,
367
- `<path d="M${fixed(x + 17.8, 3)} ${fixed(y + 6.085, 3)} L${fixed(x + 17.702, 3)} ${fixed(y + 6.113, 3)} C${fixed(x + 16.971, 3)} ${fixed(y + 6.322, 3)} ${fixed(x + 16.482, 3)} ${fixed(y + 6.851, 3)} ${fixed(x + 16.609, 3)} ${fixed(y + 7.294, 3)} L${fixed(x + 18.176, 3)} ${fixed(y + 12.759, 3)} C${fixed(x + 18.303, 3)} ${fixed(y + 13.202, 3)} ${fixed(x + 18.998, 3)} ${fixed(y + 13.391, 3)} ${fixed(x + 19.729, 3)} ${fixed(y + 13.182, 3)} L${fixed(x + 19.827, 3)} ${fixed(y + 13.154, 3)} C${fixed(x + 20.557, 3)} ${fixed(y + 12.944, 3)} ${fixed(x + 21.046, 3)} ${fixed(y + 12.415, 3)} ${fixed(x + 20.919, 3)} ${fixed(y + 11.972, 3)} L${fixed(x + 19.352, 3)} ${fixed(y + 6.507, 3)} C${fixed(x + 19.225, 3)} ${fixed(y + 6.065, 3)} ${fixed(x + 18.53, 3)} ${fixed(y + 5.875, 3)} ${fixed(x + 17.8, 3)} ${fixed(y + 6.085, 3)} Z" fill="${accent}" stroke="${BLACK}" stroke-width="1.8"/>`,
368
- `<path d="M${fixed(x + 9.546, 3)} ${fixed(y + 19.999, 3)} L${fixed(x + 6, 3)} ${fixed(y + 17.791, 3)} C${fixed(x + 4.736, 3)} ${fixed(y + 17.009, 3)} ${fixed(x + 5.668, 3)} ${fixed(y + 15.181, 3)} ${fixed(x + 7.138, 3)} ${fixed(y + 15.592, 3)} L${fixed(x + 10.615, 3)} ${fixed(y + 17.196, 3)} L${fixed(x + 9.546, 3)} ${fixed(y + 19.999, 3)} Z" fill="${accent}" stroke="${BLACK}" stroke-width="1.8" stroke-linejoin="round"/>`,
369
- `<rect x="${fixed(x + 10.887, 3)}" y="${fixed(y + 14.834, 3)}" width="9.584" height="3.800" transform="rotate(-16.9438 ${fixed(x + 10.887, 3)} ${fixed(y + 14.834, 3)})" fill="${accent}"/>`,
370
- `<path d="M${fixed(x + 10.5)} ${fixed(y + 19.5)} L${fixed(x + 11)} ${fixed(y + 20)} L${fixed(x + 11.5)} ${fixed(y + 17)} L${fixed(x + 9.5)} ${fixed(y + 17.5)} L${fixed(x + 9)} ${fixed(y + 18.5)} L${fixed(x + 10.5)} ${fixed(y + 19.5)} Z" fill="${accent}"/>`,
371
- ].join('\n');
372
- }
373
-
374
- function fallbackTagSvg(tag, x, y) {
375
- const label = fallbackTagLabel(tag);
376
- const width = tagWidth(tag);
377
- return [
378
- `<g class="tag-icon tag-fallback" role="img" aria-label="${escapeXml(label)}">`,
379
- `<title>${escapeXml(label)}</title>`,
380
- `<rect x="${fixed(x + 3)}" y="${fixed(y + 4)}" width="${width}" height="${TAG_ICON_SIZE}" rx="9" fill="${BLACK}"/>`,
381
- `<rect x="${fixed(x)}" y="${fixed(y)}" width="${width}" height="${TAG_ICON_SIZE}" rx="9" fill="#F6F1FF" stroke="${BLACK}" stroke-width="2.5"/>`,
382
- `<text x="${fixed(x + width / 2)}" y="${fixed(y + 20.5)}" text-anchor="middle" font-size="${LABEL_FONT_SIZE}" font-weight="900" fill="${BLACK}">${escapeXml(label)}</text>`,
383
- '</g>',
384
- ].join('\n');
385
- }
386
-
387
- function tagIconSvg(tag, x, y) {
388
- const normalized = tagName(tag);
389
- if (!['like', 'dislike', 'request end'].includes(normalized)) {
390
- return fallbackTagSvg(normalized, x, y);
391
- }
392
- const [fill, accent] = TAG_ICON_THEMES[normalized];
393
- const icon = normalized === 'request end'
394
- ? requestEndIconSvg(x, y, accent)
395
- : thumbIconSvg(x, y, accent, normalized === 'dislike');
396
- return [
397
- `<g class="tag-icon tag-${escapeXml(normalized.replaceAll(' ', '-'))}" role="img" aria-label="${escapeXml(normalized)}">`,
398
- `<title>${escapeXml(normalized)}</title>`,
399
- `<rect x="${fixed(x + 3)}" y="${fixed(y + 4)}" width="${TAG_ICON_SIZE}" height="${TAG_ICON_SIZE}" rx="9" fill="${BLACK}"/>`,
400
- `<rect x="${fixed(x)}" y="${fixed(y)}" width="${TAG_ICON_SIZE}" height="${TAG_ICON_SIZE}" rx="9" fill="${fill}" stroke="${BLACK}" stroke-width="2.5"/>`,
401
- icon,
402
- '</g>',
403
- ].join('\n');
404
- }
405
-
406
- function renderTagIconsSvg(tags, x, y) {
407
- const parts = [`<g class="tag-icons" transform="translate(${fixed(x)} ${fixed(y)})">`];
408
- let cursor = 0;
409
- for (const tag of tags) {
410
- parts.push(tagIconSvg(tag, cursor, 0));
411
- cursor += tagWidth(tag) + TAG_ICON_GAP;
412
- }
413
- parts.push('</g>');
414
- return parts.join('\n');
415
- }
416
-
417
- function renderMessageSvg(item) {
418
- const { message } = item;
419
- const colors = sideColors(message.side);
420
- const accessibleLabel = escapeXml(`${message.participantLabel}: ${ellipsizeText(message.text, 42)}`);
421
- const parts = [
422
- `<g class="message-row ${message.side}" role="listitem" aria-label="${accessibleLabel}">`,
423
- `<title>${accessibleLabel}</title>`,
424
- bubbleLayersSvg(item, colors),
425
- `<rect x="${item.labelX}" y="${item.labelY}" width="${item.labelWidth}" height="${LABEL_HEIGHT}" rx="9" fill="${colors.label}" stroke="${BLACK}" stroke-width="3"/>`,
426
- `<text x="${fixed(item.labelX + item.labelWidth / 2)}" y="${item.labelY + 21}" text-anchor="middle" font-size="${LABEL_FONT_SIZE}" font-weight="900" fill="${BLACK}">${escapeXml(item.label)}</text>`,
427
- ];
428
- const textX = item.bubbleX + BUBBLE_PAD_X;
429
- let textY = item.bubbleY + BUBBLE_PAD_Y + 17;
430
- for (const line of item.lines) {
431
- parts.push(`<text x="${textX}" y="${textY}" font-size="${FONT_SIZE}" font-weight="800" fill="${BLACK}">${escapeXml(line)}</text>`);
432
- textY += LINE_HEIGHT;
433
- }
434
- if (message.tags.length) {
435
- parts.push(renderTagIconsSvg(message.tags, textX, textY + TAG_ICON_TOP_GAP));
436
- }
437
- parts.push('</g>');
438
- return parts.join('\n');
439
- }
440
-
441
- export function renderTranscriptPageSvg(page) {
442
- const titleId = `claworld-report-title-${page.page}`;
443
- const descriptionId = `claworld-report-desc-${page.page}`;
444
- const description = `${page.title}. ${page.subtitle}. ${page.items.length} transcript rows.`;
445
- const parts = [
446
- `<svg class="comic-grid" xmlns="http://www.w3.org/2000/svg" width="${page.width}" height="${page.height}" viewBox="0 0 ${page.width} ${page.height}" role="img" aria-labelledby="${titleId} ${descriptionId}">`,
447
- `<title id="${titleId}">${escapeXml(page.title)}</title>`,
448
- `<desc id="${descriptionId}">${escapeXml(description)}</desc>`,
449
- svgDefs(),
450
- `<rect x="0" y="0" width="${page.width}" height="${page.height}" fill="${THEME.paper}"/>`,
451
- `<rect x="0" y="0" width="${page.width}" height="${page.height}" fill="url(#comicGridMinor)"/>`,
452
- `<rect x="0" y="0" width="${page.width}" height="${page.height}" fill="url(#comicGridMajor)" opacity="0.46"/>`,
453
- `<rect x="${FRAME_MARGIN}" y="${FRAME_MARGIN}" width="${page.width - FRAME_MARGIN * 2}" height="${page.height - FRAME_MARGIN * 2}" rx="46" fill="none" stroke="${BLACK}" stroke-width="6"/>`,
454
- renderHeader(page),
455
- '<g role="list">',
456
- ];
457
- for (const item of page.items) {
458
- if (item.kind === 'ellipsis') {
459
- const y = item.y + 7;
460
- parts.push(`<text x="${fixed(page.width / 2)}" y="${y + 14}" text-anchor="middle" font-size="${SMALL_FONT_SIZE}" fill="#555555">${escapeXml(item.label)}</text>`);
461
- } else if (item.kind === 'time') {
462
- parts.push(renderTimeSvg(page, item));
463
- } else {
464
- parts.push(renderMessageSvg(item));
465
- }
466
- }
467
- parts.push('</g>');
468
- if (page.footer) {
469
- parts.push(`<text x="${fixed(page.width / 2)}" y="${page.height - 24}" text-anchor="middle" font-size="${SMALL_FONT_SIZE}" fill="#444444">${escapeXml(page.footer)}</text>`);
470
- }
471
- parts.push('</svg>');
472
- return parts.join('\n');
473
- }
474
-
475
- export const CLAWORLD_TRANSCRIPT_STYLE_NAME = 'claworld-comic-grid';