@xfxstudio/claworld 2026.7.9-testing.1 → 2026.7.9-testing.3
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/README.md +1 -1
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/skills/claworld-main-session/SKILL.md +1 -1
- package/skills/claworld-management-session/SKILL.md +1 -1
- package/src/openclaw/plugin/register.js +20 -2
- package/src/openclaw/runtime/transcript-report.js +717 -128
package/README.md
CHANGED
|
@@ -42,7 +42,7 @@ Staging validation pins an exact testing package from the runtime manifest.
|
|
|
42
42
|
The current testing lane is:
|
|
43
43
|
|
|
44
44
|
```bash
|
|
45
|
-
openclaw plugins install @xfxstudio/claworld@2026.7.9-testing.
|
|
45
|
+
openclaw plugins install @xfxstudio/claworld@2026.7.9-testing.3
|
|
46
46
|
```
|
|
47
47
|
|
|
48
48
|
Testing packages default to `https://staging.claworld.love`; stable packages
|
package/openclaw.plugin.json
CHANGED
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
},
|
|
19
19
|
"name": "Claworld Persona Relay",
|
|
20
20
|
"description": "Claworld relay world channel plugin for OpenClaw.",
|
|
21
|
-
"version": "2026.7.9-testing.
|
|
21
|
+
"version": "2026.7.9-testing.3",
|
|
22
22
|
"configSchema": {
|
|
23
23
|
"type": "object",
|
|
24
24
|
"additionalProperties": false,
|
package/package.json
CHANGED
|
@@ -91,7 +91,7 @@ When the human asks for the full chat content, a visual chat log, screenshots, o
|
|
|
91
91
|
1. Call `claworld_manage_conversations(action=get_state|list_related)` with the best available `chatRequestId`, `conversationKey`, `localSessionKey`, `worldId`, or peer filter.
|
|
92
92
|
2. Use the returned `localTranscriptEpisodes` or `.claworld/sessions/index.json` to identify the exact `chatRequestId`.
|
|
93
93
|
3. Call `claworld_render_transcript_report(mode=stored, stored.chatRequestId=<id>, style=claworld-comic-grid)`.
|
|
94
|
-
4. Use `deliveryHint.
|
|
94
|
+
4. Use `deliveryHint.messageTool` for a single-page report or `deliveryHint.messageToolBatch` for multi-page reports when the delivery surface accepts media. These are OpenClaw `message(action=upload-file, filePath=...)` payloads, not Hermes media-token refs.
|
|
95
95
|
|
|
96
96
|
Manual mode is only for excerpts, hand-curated highlights, or fallback rendering when the local Conversation Session transcript cannot be found.
|
|
97
97
|
|
|
@@ -141,7 +141,7 @@ When the notification or owner-facing report needs the exact chat log, screensho
|
|
|
141
141
|
|
|
142
142
|
1. Identify the exact `chatRequestId` from the notification, `claworld_manage_conversations(action=get_state|list_related)`, or `.claworld/sessions/index.json`.
|
|
143
143
|
2. Call `claworld_render_transcript_report(mode=stored, stored.chatRequestId=<id>, style=claworld-comic-grid)`.
|
|
144
|
-
3. Include the returned `deliveryHint.
|
|
144
|
+
3. Include the returned `deliveryHint.messageTool` or `deliveryHint.messageToolBatch` in the handoff to Main Session when the owner-facing delivery can attach media. These are OpenClaw `message(action=upload-file, filePath=...)` payloads, not Hermes media-token refs.
|
|
145
145
|
4. Keep `artifacts.svgPages` and `artifacts.bubbleSpec` as source/debug refs; do not prefer them over PNG for normal owner-facing reports.
|
|
146
146
|
|
|
147
147
|
Use manual mode only for selected excerpts, highlights, or fallback rendering when the stored OpenClaw Conversation Session transcript is unavailable.
|
|
@@ -575,6 +575,24 @@ async function augmentConversationToolResultWithTranscripts(api, plugin, params,
|
|
|
575
575
|
});
|
|
576
576
|
}
|
|
577
577
|
|
|
578
|
+
function stripTranscriptReportToolContextParams(params = {}) {
|
|
579
|
+
const contextFields = new Set([
|
|
580
|
+
'workspaceRoot',
|
|
581
|
+
'workspaceDir',
|
|
582
|
+
'workspacePath',
|
|
583
|
+
'workspace',
|
|
584
|
+
'cwd',
|
|
585
|
+
'agent',
|
|
586
|
+
'context',
|
|
587
|
+
'session',
|
|
588
|
+
'agentId',
|
|
589
|
+
'localAgentId',
|
|
590
|
+
]);
|
|
591
|
+
return Object.fromEntries(
|
|
592
|
+
Object.entries(params || {}).filter(([key]) => !contextFields.has(key)),
|
|
593
|
+
);
|
|
594
|
+
}
|
|
595
|
+
|
|
578
596
|
function validateChatInboxFilterInput(filters = {}, action) {
|
|
579
597
|
const source = normalizeObject(filters, {}) || {};
|
|
580
598
|
for (const key of Object.keys(source)) {
|
|
@@ -1516,7 +1534,7 @@ function createTerminalToolAdapters(api, plugin, internalTools) {
|
|
|
1516
1534
|
usageNotes: [
|
|
1517
1535
|
'Use mode=stored when the exact chatRequestId is known and the local Conversation Session transcript has been indexed.',
|
|
1518
1536
|
'Use mode=manual only when the human asks for a selected excerpt or the stored transcript is unavailable.',
|
|
1519
|
-
'
|
|
1537
|
+
'Use deliveryHint.messageTool or deliveryHint.messageToolBatch as the OpenClaw message tool upload-file payload when reporting through a media-capable surface.',
|
|
1520
1538
|
'Do not paste raw transcript text to the human when this report rendering tool can produce the visual artifact.',
|
|
1521
1539
|
],
|
|
1522
1540
|
}),
|
|
@@ -1526,7 +1544,7 @@ function createTerminalToolAdapters(api, plugin, internalTools) {
|
|
|
1526
1544
|
const payload = await renderClaworldTranscriptReport({
|
|
1527
1545
|
workspaceRoot,
|
|
1528
1546
|
agentId,
|
|
1529
|
-
args: params,
|
|
1547
|
+
args: stripTranscriptReportToolContextParams(params),
|
|
1530
1548
|
});
|
|
1531
1549
|
return buildToolResult({
|
|
1532
1550
|
...payload,
|
|
@@ -31,6 +31,82 @@ const MANUAL_MESSAGE_FIELDS = new Set(['from', 'text', 'createdAt']);
|
|
|
31
31
|
const TIME_SPLIT_SECONDS = 5 * 60;
|
|
32
32
|
const SEGMENT_GAP_SECONDS = 4 * 60 * 60;
|
|
33
33
|
const ARTIFACT_SCHEMA = 'claworld.transcript_report.v1';
|
|
34
|
+
const CANVAS_MARGIN = 24;
|
|
35
|
+
const FRAME_MARGIN = 16;
|
|
36
|
+
const HEADER_Y = 48;
|
|
37
|
+
const HEADER_CARD_HEIGHT_ONE_LINE = 92;
|
|
38
|
+
const HEADER_CARD_HEIGHT_TWO_LINES = 110;
|
|
39
|
+
const HEADER_BOTTOM_PAD = 20;
|
|
40
|
+
const BODY_TOP_GAP = 24;
|
|
41
|
+
const PAGE_BOTTOM = 54;
|
|
42
|
+
const ITEM_GAP = 22;
|
|
43
|
+
const TIME_ROW_HEIGHT = 42;
|
|
44
|
+
const ELLIPSIS_HEIGHT = 34;
|
|
45
|
+
const BUBBLE_PAD_X = 32;
|
|
46
|
+
const BUBBLE_PAD_Y = 22;
|
|
47
|
+
const LABEL_HEIGHT = 30;
|
|
48
|
+
const LABEL_OVERLAP = 18;
|
|
49
|
+
const LABEL_RAISE = 9;
|
|
50
|
+
const LABEL_MAX_COLS = 14;
|
|
51
|
+
const BUBBLE_MAX_RATIO = 0.64;
|
|
52
|
+
const BUBBLE_MIN_WIDTH = 200;
|
|
53
|
+
const FONT_SIZE = 18;
|
|
54
|
+
const SMALL_FONT_SIZE = 12;
|
|
55
|
+
const LABEL_FONT_SIZE = 15;
|
|
56
|
+
const TITLE_FONT_SIZE = 34;
|
|
57
|
+
const LINE_HEIGHT = 29;
|
|
58
|
+
const HEADER_SUBTITLE_MAX_UNITS = 33;
|
|
59
|
+
const HEADER_SUBTITLE_MAX_LINES = 2;
|
|
60
|
+
const HEADER_SUBTITLE_LINE_HEIGHT = 19;
|
|
61
|
+
const TAG_HEIGHT = 58;
|
|
62
|
+
const TAG_ICON_SIZE = 30;
|
|
63
|
+
const TAG_ICON_GAP = 12;
|
|
64
|
+
const TAG_ICON_TOP_GAP = 8;
|
|
65
|
+
const TAG_FALLBACK_MAX_COLS = 10;
|
|
66
|
+
const TEXT_UNIT_PX = 18;
|
|
67
|
+
const BLACK = '#090909';
|
|
68
|
+
const COMIC_THEME = Object.freeze({
|
|
69
|
+
paper: '#FBF8EF',
|
|
70
|
+
paperWarm: '#FFFDF7',
|
|
71
|
+
headerFill: '#FEF5D8',
|
|
72
|
+
gridMinor: '#BED1D8',
|
|
73
|
+
gridMajor: '#AABFC8',
|
|
74
|
+
ink: BLACK,
|
|
75
|
+
muted: '#222222',
|
|
76
|
+
leftFill: '#EFFFF5',
|
|
77
|
+
leftLabel: '#62E69D',
|
|
78
|
+
leftAccentA: '#58E58F',
|
|
79
|
+
leftAccentB: '#47B6FF',
|
|
80
|
+
rightFill: '#EFE0FF',
|
|
81
|
+
rightLabel: '#B785FF',
|
|
82
|
+
rightAccentA: '#A871FF',
|
|
83
|
+
rightAccentB: '#FF4EB4',
|
|
84
|
+
timeFill: '#FFFDF7',
|
|
85
|
+
timeAccentLeft: '#FF62DE',
|
|
86
|
+
timeAccentRight: '#50D995',
|
|
87
|
+
});
|
|
88
|
+
const TAG_ICON_THEMES = Object.freeze({
|
|
89
|
+
like: ['#D8F4FF', '#58B7FF'],
|
|
90
|
+
dislike: ['#FFE0EA', '#FF6A9A'],
|
|
91
|
+
'request end': ['#FFF0A8', '#FF9F2F'],
|
|
92
|
+
});
|
|
93
|
+
const COMIC_FONT_FAMILY = [
|
|
94
|
+
"'PingFang SC'",
|
|
95
|
+
"'Hiragino Sans GB'",
|
|
96
|
+
"'Microsoft YaHei'",
|
|
97
|
+
"'WenQuanYi Zen Hei'",
|
|
98
|
+
"'Noto Sans CJK SC'",
|
|
99
|
+
"'Noto Sans SC'",
|
|
100
|
+
"'Source Han Sans SC'",
|
|
101
|
+
"'IPA P Gothic'",
|
|
102
|
+
"'AR PL UMing CN'",
|
|
103
|
+
"'Arial Unicode MS'",
|
|
104
|
+
'-apple-system',
|
|
105
|
+
'BlinkMacSystemFont',
|
|
106
|
+
"'Segoe UI'",
|
|
107
|
+
'Arial',
|
|
108
|
+
'sans-serif',
|
|
109
|
+
].join(', ');
|
|
34
110
|
|
|
35
111
|
function isObject(value) {
|
|
36
112
|
return value && typeof value === 'object' && !Array.isArray(value);
|
|
@@ -294,15 +370,40 @@ function normalizeTagLabel(value) {
|
|
|
294
370
|
}
|
|
295
371
|
|
|
296
372
|
function extractControlTags(text) {
|
|
373
|
+
const source = String(text || '');
|
|
374
|
+
const matches = [];
|
|
375
|
+
const claimedSpans = [];
|
|
297
376
|
const tags = [];
|
|
298
|
-
const
|
|
299
|
-
|
|
300
|
-
if (
|
|
301
|
-
|
|
302
|
-
|
|
377
|
+
const record = (start, end, label) => {
|
|
378
|
+
if (!label) return;
|
|
379
|
+
if (claimedSpans.some(([claimedStart, claimedEnd]) => start < claimedEnd && end > claimedStart)) return;
|
|
380
|
+
matches.push([start, end, label]);
|
|
381
|
+
claimedSpans.push([start, end]);
|
|
382
|
+
};
|
|
383
|
+
const controlPatterns = [
|
|
384
|
+
{ pattern: /\[\[?\s*request[_\s-]*(?:conversation[_\s-]*)?end\s*\]?\]?/gi, label: 'request end' },
|
|
385
|
+
{ pattern: /\[\s*requeset\s+end\s*\]/gi, label: 'request end' },
|
|
386
|
+
{ pattern: /\[\[?\s*end\s*\]?\]?/gi, label: 'request end' },
|
|
387
|
+
{ pattern: /\[\[?\s*like\s*\]?\]?/gi, label: 'like' },
|
|
388
|
+
{ pattern: /\[\[?\s*dislike\s*\]?\]?/gi, label: 'dislike' },
|
|
389
|
+
];
|
|
390
|
+
for (const { pattern, label } of controlPatterns) {
|
|
391
|
+
for (const match of source.matchAll(pattern)) record(match.index, match.index + match[0].length, label);
|
|
392
|
+
}
|
|
393
|
+
for (const match of source.matchAll(/\[\[\s*([A-Za-z0-9][A-Za-z0-9 _-]{0,24})\s*\]\]/g)) {
|
|
394
|
+
record(match.index, match.index + match[0].length, normalizeTagLabel(match[1]));
|
|
395
|
+
}
|
|
396
|
+
let cleaned = '';
|
|
397
|
+
let cursor = 0;
|
|
398
|
+
for (const [start, end, label] of matches.sort((left, right) => left[0] - right[0])) {
|
|
399
|
+
cleaned += source.slice(cursor, start);
|
|
400
|
+
cursor = Math.max(cursor, end);
|
|
401
|
+
if (!tags.includes(label)) tags.push(label);
|
|
402
|
+
}
|
|
403
|
+
cleaned += source.slice(cursor);
|
|
303
404
|
return {
|
|
304
405
|
text: cleaned.replace(/[ \t]+\n/g, '\n').replace(/\n{3,}/g, '\n\n').trim(),
|
|
305
|
-
tags
|
|
406
|
+
tags,
|
|
306
407
|
};
|
|
307
408
|
}
|
|
308
409
|
|
|
@@ -683,40 +784,165 @@ function normalizeHeaderContext({ mode, manual = null, source = null, messages =
|
|
|
683
784
|
};
|
|
684
785
|
}
|
|
685
786
|
|
|
787
|
+
function isWideChar(char) {
|
|
788
|
+
const code = char.codePointAt(0) || 0;
|
|
789
|
+
return (
|
|
790
|
+
(code >= 0x1100 && code <= 0x115f)
|
|
791
|
+
|| code === 0x2329
|
|
792
|
+
|| code === 0x232a
|
|
793
|
+
|| (code >= 0x2e80 && code <= 0xa4cf)
|
|
794
|
+
|| (code >= 0xac00 && code <= 0xd7a3)
|
|
795
|
+
|| (code >= 0xf900 && code <= 0xfaff)
|
|
796
|
+
|| (code >= 0xfe10 && code <= 0xfe19)
|
|
797
|
+
|| (code >= 0xfe30 && code <= 0xfe6f)
|
|
798
|
+
|| (code >= 0xff00 && code <= 0xff60)
|
|
799
|
+
|| (code >= 0xffe0 && code <= 0xffe6)
|
|
800
|
+
|| (code >= 0x1f300 && code <= 0x1faff)
|
|
801
|
+
|| (code >= 0x20000 && code <= 0x3fffd)
|
|
802
|
+
);
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
function charColumns(char) {
|
|
806
|
+
if (char === '\n') return 0;
|
|
807
|
+
return isWideChar(char) ? 2 : 1;
|
|
808
|
+
}
|
|
809
|
+
|
|
686
810
|
function displayColumns(text) {
|
|
687
811
|
let columns = 0;
|
|
688
|
-
for (const char of String(text || ''))
|
|
689
|
-
const code = char.codePointAt(0);
|
|
690
|
-
columns += code > 0x2e80 ? 2 : 1;
|
|
691
|
-
}
|
|
812
|
+
for (const char of String(text || '')) columns += charColumns(char);
|
|
692
813
|
return columns;
|
|
693
814
|
}
|
|
694
815
|
|
|
695
|
-
function
|
|
696
|
-
const
|
|
816
|
+
function clipDisplay(text, maxColumns) {
|
|
817
|
+
const value = String(text || '');
|
|
818
|
+
if (displayColumns(value) <= maxColumns) return value;
|
|
819
|
+
const suffix = '...';
|
|
820
|
+
const target = Math.max(0, maxColumns - suffix.length);
|
|
821
|
+
let kept = '';
|
|
822
|
+
let used = 0;
|
|
823
|
+
for (const char of value) {
|
|
824
|
+
const columns = charColumns(char);
|
|
825
|
+
if (used + columns > target) break;
|
|
826
|
+
kept += char;
|
|
827
|
+
used += columns;
|
|
828
|
+
}
|
|
829
|
+
return `${kept.trimEnd()}${suffix}`;
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
function charUnits(char) {
|
|
833
|
+
if (char === '\n') return 0;
|
|
834
|
+
if (/\s/u.test(char)) return 0.35;
|
|
835
|
+
return isWideChar(char) ? 1 : 0.55;
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
function textUnits(text) {
|
|
839
|
+
let units = 0;
|
|
840
|
+
for (const char of String(text || '')) units += charUnits(char);
|
|
841
|
+
return units;
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
function ellipsizeText(text, maxUnits, { suffix = '...' } = {}) {
|
|
845
|
+
const value = String(text || '');
|
|
846
|
+
if (textUnits(value) <= maxUnits) return value;
|
|
847
|
+
const allowed = Math.max(0, maxUnits - textUnits(suffix));
|
|
848
|
+
let kept = '';
|
|
849
|
+
let used = 0;
|
|
850
|
+
for (const char of value) {
|
|
851
|
+
const units = charUnits(char);
|
|
852
|
+
if (used + units > allowed) break;
|
|
853
|
+
kept += char;
|
|
854
|
+
used += units;
|
|
855
|
+
}
|
|
856
|
+
return `${kept.trimEnd()}${suffix}`;
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
function wrapTokens(paragraph) {
|
|
860
|
+
const tokens = [];
|
|
861
|
+
let current = '';
|
|
862
|
+
const flush = () => {
|
|
863
|
+
if (current) {
|
|
864
|
+
tokens.push(current);
|
|
865
|
+
current = '';
|
|
866
|
+
}
|
|
867
|
+
};
|
|
868
|
+
for (const char of String(paragraph || '')) {
|
|
869
|
+
if (/\s/u.test(char)) {
|
|
870
|
+
flush();
|
|
871
|
+
tokens.push(' ');
|
|
872
|
+
} else if (isWideChar(char)) {
|
|
873
|
+
flush();
|
|
874
|
+
tokens.push(char);
|
|
875
|
+
} else {
|
|
876
|
+
current += char;
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
flush();
|
|
880
|
+
return tokens;
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
function wrapText(text, maxUnits) {
|
|
697
884
|
const lines = [];
|
|
698
|
-
|
|
885
|
+
const paragraphs = String(text || '').replace(/\r/g, '').split('\n');
|
|
886
|
+
for (const paragraph of paragraphs.length ? paragraphs : ['']) {
|
|
699
887
|
let current = '';
|
|
700
|
-
let
|
|
701
|
-
|
|
702
|
-
for (const
|
|
703
|
-
const
|
|
704
|
-
if (
|
|
888
|
+
let currentUnits = 0;
|
|
889
|
+
let paragraphHasLine = false;
|
|
890
|
+
for (const token of wrapTokens(paragraph)) {
|
|
891
|
+
const tokenUnits = textUnits(token);
|
|
892
|
+
if (/^\s+$/u.test(token)) {
|
|
893
|
+
if (current && currentUnits + tokenUnits <= maxUnits) {
|
|
894
|
+
current += token;
|
|
895
|
+
currentUnits += tokenUnits;
|
|
896
|
+
}
|
|
897
|
+
continue;
|
|
898
|
+
}
|
|
899
|
+
if (current && currentUnits + tokenUnits > maxUnits) {
|
|
705
900
|
lines.push(current.trimEnd());
|
|
901
|
+
paragraphHasLine = true;
|
|
706
902
|
current = '';
|
|
707
|
-
|
|
903
|
+
currentUnits = 0;
|
|
904
|
+
}
|
|
905
|
+
if (tokenUnits > maxUnits) {
|
|
906
|
+
for (const char of token) {
|
|
907
|
+
const units = charUnits(char);
|
|
908
|
+
if (current && currentUnits + units > maxUnits) {
|
|
909
|
+
lines.push(current.trimEnd());
|
|
910
|
+
paragraphHasLine = true;
|
|
911
|
+
current = '';
|
|
912
|
+
currentUnits = 0;
|
|
913
|
+
}
|
|
914
|
+
current += char;
|
|
915
|
+
currentUnits += units;
|
|
916
|
+
}
|
|
917
|
+
} else {
|
|
918
|
+
current += token;
|
|
919
|
+
currentUnits += tokenUnits;
|
|
708
920
|
}
|
|
709
|
-
current += word;
|
|
710
|
-
columns += wordColumns;
|
|
711
921
|
}
|
|
712
|
-
if (current) lines.push(current.trimEnd());
|
|
713
|
-
if (!paragraph) lines.push('');
|
|
922
|
+
if (current || !paragraphHasLine) lines.push(current.trimEnd());
|
|
714
923
|
}
|
|
715
924
|
return lines.length > 0 ? lines : [''];
|
|
716
925
|
}
|
|
717
926
|
|
|
927
|
+
function formatCompactTimestamp(value) {
|
|
928
|
+
const ms = parseTimestampMs(value);
|
|
929
|
+
if (ms) {
|
|
930
|
+
const date = new Date(ms);
|
|
931
|
+
const pad = (part) => String(part).padStart(2, '0');
|
|
932
|
+
return `${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}`;
|
|
933
|
+
}
|
|
934
|
+
const text = String(value || '').trim();
|
|
935
|
+
const match = text.match(/(?:(\d{4})[-/])?(\d{1,2})[-/](\d{1,2})[ T](\d{1,2}):(\d{2})/);
|
|
936
|
+
if (match) {
|
|
937
|
+
return `${String(Number(match[2])).padStart(2, '0')}-${String(Number(match[3])).padStart(2, '0')} ${String(Number(match[4])).padStart(2, '0')}:${match[5]}`;
|
|
938
|
+
}
|
|
939
|
+
return '';
|
|
940
|
+
}
|
|
941
|
+
|
|
718
942
|
function timeLabelForMessage(message) {
|
|
719
|
-
return message.
|
|
943
|
+
return formatCompactTimestamp(message.createdAt || message.timestampMs || message.createdAtLabel)
|
|
944
|
+
|| message.createdAtLabel
|
|
945
|
+
|| '';
|
|
720
946
|
}
|
|
721
947
|
|
|
722
948
|
function buildRenderItems(messages = []) {
|
|
@@ -741,76 +967,162 @@ function buildRenderItems(messages = []) {
|
|
|
741
967
|
return items;
|
|
742
968
|
}
|
|
743
969
|
|
|
744
|
-
function
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
970
|
+
function renderSide(message = {}) {
|
|
971
|
+
return message.side === 'local' || message.side === 'right' || message.side === 'me'
|
|
972
|
+
? 'right'
|
|
973
|
+
: 'left';
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
function participantLabel(message = {}, header = {}) {
|
|
977
|
+
if (renderSide(message) === 'right') return header.localLabel || 'You';
|
|
978
|
+
return message.speaker || header.peerLabel || 'Peer';
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
function messageTags(message = {}) {
|
|
982
|
+
return Array.isArray(message.tags) ? message.tags : [];
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
function measureRenderItem(item, width, header) {
|
|
986
|
+
const contentWidth = Math.max(270, Math.trunc(width * BUBBLE_MAX_RATIO));
|
|
987
|
+
if (item.kind === 'ellipsis') {
|
|
988
|
+
return {
|
|
989
|
+
kind: 'ellipsis',
|
|
990
|
+
message: null,
|
|
991
|
+
lines: [],
|
|
992
|
+
width: contentWidth,
|
|
993
|
+
height: ELLIPSIS_HEIGHT,
|
|
994
|
+
omittedCount: item.omitted || 0,
|
|
995
|
+
label: item.label || '',
|
|
996
|
+
};
|
|
997
|
+
}
|
|
998
|
+
if (item.kind === 'time') {
|
|
999
|
+
return {
|
|
1000
|
+
kind: 'time',
|
|
1001
|
+
message: null,
|
|
1002
|
+
lines: [],
|
|
1003
|
+
width: contentWidth,
|
|
1004
|
+
height: TIME_ROW_HEIGHT,
|
|
1005
|
+
label: item.label || '',
|
|
1006
|
+
};
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
const side = renderSide(item.message);
|
|
1010
|
+
const label = participantLabel(item.message, header);
|
|
1011
|
+
const maxUnits = Math.max(18, (contentWidth - BUBBLE_PAD_X * 2) / TEXT_UNIT_PX);
|
|
1012
|
+
const lines = wrapText(item.message.text, maxUnits);
|
|
1013
|
+
const labelUnits = textUnits(labelText(label)) + 4;
|
|
1014
|
+
const contentUnits = Math.max(
|
|
1015
|
+
...lines.map((line) => textUnits(line)),
|
|
1016
|
+
tagRowUnits(messageTags(item.message)),
|
|
1017
|
+
labelUnits,
|
|
1018
|
+
10,
|
|
751
1019
|
);
|
|
752
|
-
const
|
|
1020
|
+
const bubbleWidth = Math.trunc(Math.min(
|
|
1021
|
+
contentWidth,
|
|
1022
|
+
Math.max(BUBBLE_MIN_WIDTH, contentUnits * TEXT_UNIT_PX + BUBBLE_PAD_X * 2),
|
|
1023
|
+
));
|
|
1024
|
+
const textHeight = lines.length * LINE_HEIGHT;
|
|
1025
|
+
const tagHeight = messageTags(item.message).length ? TAG_HEIGHT : 0;
|
|
1026
|
+
const bubbleHeight = BUBBLE_PAD_Y * 2 + textHeight + tagHeight;
|
|
1027
|
+
const rowHeight = LABEL_HEIGHT - LABEL_OVERLAP + bubbleHeight + 10;
|
|
753
1028
|
return {
|
|
1029
|
+
kind: 'message',
|
|
1030
|
+
message: item.message,
|
|
1031
|
+
side,
|
|
1032
|
+
participantLabel: label,
|
|
754
1033
|
lines,
|
|
755
|
-
width:
|
|
756
|
-
height,
|
|
1034
|
+
width: bubbleWidth,
|
|
1035
|
+
height: rowHeight,
|
|
1036
|
+
tagHeight,
|
|
1037
|
+
textHeight,
|
|
757
1038
|
};
|
|
758
1039
|
}
|
|
759
1040
|
|
|
760
1041
|
function layoutPages({ messages, header, maxPageHeight }) {
|
|
761
1042
|
const width = DEFAULT_WIDTH;
|
|
762
1043
|
const maxHeight = maxPageHeight || DEFAULT_MAX_PAGE_HEIGHT;
|
|
763
|
-
const
|
|
764
|
-
const
|
|
765
|
-
const
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
1044
|
+
const measuredItems = buildRenderItems(messages).map((item) => measureRenderItem(item, width, header));
|
|
1045
|
+
const pageBuckets = [[]];
|
|
1046
|
+
const headerHeight = headerHeightForSubtitle(header.subtitle);
|
|
1047
|
+
let used = headerHeight + BODY_TOP_GAP + PAGE_BOTTOM;
|
|
1048
|
+
|
|
1049
|
+
measuredItems.forEach((item, index) => {
|
|
1050
|
+
const itemHeight = item.height + ITEM_GAP;
|
|
1051
|
+
let neededHeight = itemHeight;
|
|
1052
|
+
if (item.kind === 'time' && index + 1 < measuredItems.length) {
|
|
1053
|
+
neededHeight += measuredItems[index + 1].height + ITEM_GAP;
|
|
1054
|
+
}
|
|
1055
|
+
const currentPage = pageBuckets[pageBuckets.length - 1];
|
|
1056
|
+
if (currentPage.length > 0 && used + neededHeight > maxHeight) {
|
|
1057
|
+
pageBuckets.push([]);
|
|
1058
|
+
used = headerHeight + BODY_TOP_GAP + PAGE_BOTTOM;
|
|
1059
|
+
}
|
|
1060
|
+
pageBuckets[pageBuckets.length - 1].push(item);
|
|
1061
|
+
used += itemHeight;
|
|
1062
|
+
});
|
|
774
1063
|
|
|
775
|
-
const
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
1064
|
+
const pages = pageBuckets.map((pageItems, pageIndex) => {
|
|
1065
|
+
let y = headerHeight + BODY_TOP_GAP;
|
|
1066
|
+
const elements = [];
|
|
1067
|
+
for (const item of pageItems) {
|
|
1068
|
+
if (item.kind === 'ellipsis') {
|
|
1069
|
+
elements.push({
|
|
1070
|
+
kind: 'ellipsis',
|
|
1071
|
+
y,
|
|
1072
|
+
height: item.height,
|
|
1073
|
+
label: item.label,
|
|
1074
|
+
});
|
|
1075
|
+
y += item.height + ITEM_GAP;
|
|
1076
|
+
continue;
|
|
1077
|
+
}
|
|
1078
|
+
if (item.kind === 'time') {
|
|
1079
|
+
elements.push({
|
|
1080
|
+
kind: 'time',
|
|
1081
|
+
y,
|
|
1082
|
+
height: item.height,
|
|
1083
|
+
label: item.label,
|
|
1084
|
+
});
|
|
1085
|
+
y += item.height + ITEM_GAP;
|
|
1086
|
+
continue;
|
|
1087
|
+
}
|
|
1088
|
+
const label = labelText(item.participantLabel);
|
|
1089
|
+
const position = positions(width, item.width, label, item.side);
|
|
1090
|
+
const bubbleY = y + LABEL_HEIGHT - LABEL_OVERLAP;
|
|
1091
|
+
const bubbleHeight = BUBBLE_PAD_Y * 2 + item.textHeight + item.tagHeight;
|
|
1092
|
+
elements.push({
|
|
1093
|
+
kind: 'message',
|
|
1094
|
+
y,
|
|
1095
|
+
x: position.bubbleX,
|
|
1096
|
+
bubbleX: position.bubbleX,
|
|
1097
|
+
bubbleY,
|
|
1098
|
+
labelX: position.labelX,
|
|
1099
|
+
labelY: y - LABEL_RAISE,
|
|
1100
|
+
labelWidth: position.labelWidth,
|
|
1101
|
+
label,
|
|
1102
|
+
align: position.align,
|
|
1103
|
+
width: item.width,
|
|
1104
|
+
height: bubbleHeight,
|
|
1105
|
+
bubbleHeight,
|
|
1106
|
+
lines: item.lines,
|
|
1107
|
+
message: item.message,
|
|
1108
|
+
side: item.side,
|
|
1109
|
+
participantLabel: item.participantLabel,
|
|
1110
|
+
tagHeight: item.tagHeight,
|
|
1111
|
+
textHeight: item.textHeight,
|
|
1112
|
+
});
|
|
1113
|
+
y += item.height + ITEM_GAP;
|
|
1114
|
+
}
|
|
1115
|
+
return {
|
|
1116
|
+
page: pageIndex + 1,
|
|
780
1117
|
width,
|
|
781
|
-
height:
|
|
782
|
-
elements
|
|
1118
|
+
height: Math.max(520, Math.min(maxHeight, Math.ceil(y + PAGE_BOTTOM))),
|
|
1119
|
+
elements,
|
|
1120
|
+
title: header.title,
|
|
1121
|
+
subtitle: header.subtitle,
|
|
1122
|
+
footer: 'visit claworld.love',
|
|
783
1123
|
};
|
|
784
|
-
|
|
785
|
-
};
|
|
1124
|
+
});
|
|
786
1125
|
|
|
787
|
-
for (const item of buildRenderItems(messages)) {
|
|
788
|
-
if (item.kind === 'time') {
|
|
789
|
-
const height = 28;
|
|
790
|
-
if (y + height + bottomMargin > maxHeight && page.elements.length > 0) pushPage();
|
|
791
|
-
page.elements.push({ ...item, x: margin, y, width: width - margin * 2, height });
|
|
792
|
-
y += height;
|
|
793
|
-
continue;
|
|
794
|
-
}
|
|
795
|
-
const messageLayout = estimateMessageLayout(item.message);
|
|
796
|
-
const height = messageLayout.height + 10;
|
|
797
|
-
if (y + height + bottomMargin > maxHeight && page.elements.length > 0) pushPage();
|
|
798
|
-
const x = item.message.side === 'local'
|
|
799
|
-
? width - margin - messageLayout.width
|
|
800
|
-
: margin;
|
|
801
|
-
page.elements.push({
|
|
802
|
-
kind: 'message',
|
|
803
|
-
message: item.message,
|
|
804
|
-
layout: messageLayout,
|
|
805
|
-
x,
|
|
806
|
-
y,
|
|
807
|
-
width: messageLayout.width,
|
|
808
|
-
height: messageLayout.height,
|
|
809
|
-
});
|
|
810
|
-
y += height;
|
|
811
|
-
}
|
|
812
|
-
page.height = Math.min(maxHeight, Math.max(MIN_PAGE_HEIGHT, Math.ceil(y + bottomMargin)));
|
|
813
|
-
pages.push(page);
|
|
814
1126
|
return {
|
|
815
1127
|
width,
|
|
816
1128
|
header,
|
|
@@ -826,58 +1138,314 @@ function escapeXml(value) {
|
|
|
826
1138
|
.replace(/"/g, '"');
|
|
827
1139
|
}
|
|
828
1140
|
|
|
829
|
-
function renderSvgPage({ page
|
|
830
|
-
const
|
|
831
|
-
const
|
|
1141
|
+
function renderSvgPage({ page }) {
|
|
1142
|
+
const titleId = `claworld-report-title-${page.page}`;
|
|
1143
|
+
const descId = `claworld-report-desc-${page.page}`;
|
|
1144
|
+
const desc = `${page.title}. ${page.subtitle}. ${page.elements.length} transcript rows.`;
|
|
832
1145
|
const parts = [
|
|
833
|
-
`<svg xmlns="http://www.w3.org/2000/svg" width="${page.width}" height="${page.height}" viewBox="0 0 ${page.width} ${page.height}" role="img" aria-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
`<rect
|
|
838
|
-
`<rect
|
|
839
|
-
`<
|
|
840
|
-
`<
|
|
1146
|
+
`<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} ${descId}">`,
|
|
1147
|
+
`<title id="${titleId}">${escapeXml(page.title)}</title>`,
|
|
1148
|
+
`<desc id="${descId}">${escapeXml(desc)}</desc>`,
|
|
1149
|
+
svgDefs(),
|
|
1150
|
+
`<rect x="0" y="0" width="${page.width}" height="${page.height}" fill="${COMIC_THEME.paper}"/>`,
|
|
1151
|
+
`<rect x="0" y="0" width="${page.width}" height="${page.height}" fill="url(#comicGridMinor)"/>`,
|
|
1152
|
+
`<rect x="0" y="0" width="${page.width}" height="${page.height}" fill="url(#comicGridMajor)" opacity="0.46"/>`,
|
|
1153
|
+
`<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"/>`,
|
|
1154
|
+
renderHeaderSvg(page),
|
|
1155
|
+
'<g role="list">',
|
|
841
1156
|
];
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
parts.push(`<text class="time" x="${page.width / 2}" y="${element.y + 18}" text-anchor="middle">${escapeXml(element.label)}</text>`);
|
|
1157
|
+
for (const item of page.elements) {
|
|
1158
|
+
if (item.kind === 'ellipsis') {
|
|
1159
|
+
parts.push(renderEllipsisSvg(page, item));
|
|
846
1160
|
continue;
|
|
847
1161
|
}
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
parts.push(`<text class="label" x="${x}" y="${y + 10}">${escapeXml(label)}</text>`);
|
|
852
|
-
parts.push(`<rect class="shadow" x="${x + 4}" y="${y + 18}" width="${width}" height="${height}" rx="16"/>`);
|
|
853
|
-
parts.push(`<rect class="${bubbleClass}" x="${x}" y="${y + 14}" width="${width}" height="${height}" rx="16"/>`);
|
|
854
|
-
const textX = x + 17;
|
|
855
|
-
let textY = y + 40;
|
|
856
|
-
parts.push(`<text class="msg" x="${textX}" y="${textY}">`);
|
|
857
|
-
for (const [index, line] of layout.lines.entries()) {
|
|
858
|
-
parts.push(`<tspan x="${textX}" dy="${index === 0 ? 0 : 20}">${escapeXml(line)}</tspan>`);
|
|
859
|
-
}
|
|
860
|
-
parts.push('</text>');
|
|
861
|
-
textY += layout.lines.length * 20 + 4;
|
|
862
|
-
let tagX = textX;
|
|
863
|
-
for (const tag of message.tags) {
|
|
864
|
-
const tagWidth = Math.max(48, displayColumns(tag) * 7 + 24);
|
|
865
|
-
const className = tag === 'request end'
|
|
866
|
-
? 'tagEnd'
|
|
867
|
-
: tag === 'like'
|
|
868
|
-
? 'tagLike'
|
|
869
|
-
: tag === 'dislike'
|
|
870
|
-
? 'tagDislike'
|
|
871
|
-
: 'tagBox';
|
|
872
|
-
parts.push(`<rect class="${className}" x="${tagX}" y="${textY}" width="${tagWidth}" height="18" rx="9"/>`);
|
|
873
|
-
parts.push(`<text class="tag" x="${tagX + 12}" y="${textY + 13}">${escapeXml(tag)}</text>`);
|
|
874
|
-
tagX += tagWidth + 6;
|
|
1162
|
+
if (item.kind === 'time') {
|
|
1163
|
+
parts.push(renderTimeSvg(page, item));
|
|
1164
|
+
continue;
|
|
875
1165
|
}
|
|
1166
|
+
parts.push(renderMessageSvg(item));
|
|
1167
|
+
}
|
|
1168
|
+
parts.push('</g>');
|
|
1169
|
+
if (page.footer) {
|
|
1170
|
+
parts.push(`<text x="${(page.width / 2).toFixed(1)}" y="${page.height - 24}" text-anchor="middle" font-size="${SMALL_FONT_SIZE}" fill="#444444">${escapeXml(page.footer)}</text>`);
|
|
876
1171
|
}
|
|
877
1172
|
parts.push('</svg>');
|
|
878
1173
|
return parts.join('\n');
|
|
879
1174
|
}
|
|
880
1175
|
|
|
1176
|
+
function positions(width, bubbleWidth, label, side) {
|
|
1177
|
+
const labelWidth = labelWidthForText(label);
|
|
1178
|
+
const inset = CANVAS_MARGIN + 38;
|
|
1179
|
+
if (side === 'right') {
|
|
1180
|
+
const bubbleX = width - inset - bubbleWidth;
|
|
1181
|
+
const labelX = bubbleX + bubbleWidth - labelWidth - 16;
|
|
1182
|
+
return { bubbleX, labelX, labelWidth, align: 'right' };
|
|
1183
|
+
}
|
|
1184
|
+
const bubbleX = inset;
|
|
1185
|
+
const labelX = bubbleX + 16;
|
|
1186
|
+
return { bubbleX, labelX, labelWidth, align: 'left' };
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1189
|
+
function renderHeaderSvg(page) {
|
|
1190
|
+
const x = CANVAS_MARGIN + 26;
|
|
1191
|
+
const y = HEADER_Y;
|
|
1192
|
+
const width = page.width - (CANVAS_MARGIN + 26) * 2;
|
|
1193
|
+
const height = headerCardHeight(page.subtitle);
|
|
1194
|
+
const title = clipDisplay(headerTitle(page.title), 32);
|
|
1195
|
+
return [
|
|
1196
|
+
`<rect x="${x + 11}" y="${y + 6}" width="${width + 2}" height="${height + 10}" rx="22" fill="${BLACK}"/>`,
|
|
1197
|
+
`<rect x="${x + 7}" y="${y + 6}" width="${width}" height="${height + 4}" rx="22" fill="url(#headerAccent)"/>`,
|
|
1198
|
+
`<rect x="${x}" y="${y}" width="${width}" height="${height}" rx="22" fill="${COMIC_THEME.headerFill}" stroke="${BLACK}" stroke-width="4"/>`,
|
|
1199
|
+
`<text x="${x + 28}" y="${y + 43}" font-size="${TITLE_FONT_SIZE}" font-weight="900" fill="${BLACK}">${escapeXml(title)}</text>`,
|
|
1200
|
+
renderHeaderSubtitleSvg(x + 35, y + 70, headerSubtitleLines(page.subtitle)),
|
|
1201
|
+
decorativeStarSvg(x + width - 62, y + 34, 22, '#FFFFFF', 'url(#headerAccent)'),
|
|
1202
|
+
`<circle cx="${x + width - 23}" cy="${y + 55}" r="9" fill="#72E3C0" stroke="${BLACK}" stroke-width="3"/>`,
|
|
1203
|
+
`<circle cx="${x + width - 25}" cy="${y + 53}" r="9" fill="#72E3C0" stroke="${BLACK}" stroke-width="3"/>`,
|
|
1204
|
+
].join('\n');
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1207
|
+
function headerSubtitleLines(subtitle) {
|
|
1208
|
+
const lines = wrapText(String(subtitle || '').trim(), HEADER_SUBTITLE_MAX_UNITS);
|
|
1209
|
+
if (lines.length <= HEADER_SUBTITLE_MAX_LINES) return lines;
|
|
1210
|
+
const visible = lines.slice(0, HEADER_SUBTITLE_MAX_LINES - 1);
|
|
1211
|
+
const remainder = lines.slice(HEADER_SUBTITLE_MAX_LINES - 1).map((line) => line.trim()).filter(Boolean).join(' ');
|
|
1212
|
+
visible.push(ellipsizeText(remainder, HEADER_SUBTITLE_MAX_UNITS));
|
|
1213
|
+
return visible;
|
|
1214
|
+
}
|
|
1215
|
+
|
|
1216
|
+
function headerCardHeight(subtitle) {
|
|
1217
|
+
return headerSubtitleLines(subtitle).length > 1
|
|
1218
|
+
? HEADER_CARD_HEIGHT_TWO_LINES
|
|
1219
|
+
: HEADER_CARD_HEIGHT_ONE_LINE;
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1222
|
+
function headerHeightForSubtitle(subtitle) {
|
|
1223
|
+
return HEADER_Y + headerCardHeight(subtitle) + HEADER_BOTTOM_PAD;
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1226
|
+
function renderHeaderSubtitleSvg(x, y, lines) {
|
|
1227
|
+
return lines.map((line, index) => (
|
|
1228
|
+
`<text class="header-subtitle-line" x="${x.toFixed(1)}" y="${(y + index * HEADER_SUBTITLE_LINE_HEIGHT).toFixed(1)}" font-size="15" font-weight="600" fill="${COMIC_THEME.muted}">${escapeXml(line)}</text>`
|
|
1229
|
+
)).join('\n');
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1232
|
+
function renderEllipsisSvg(page, item) {
|
|
1233
|
+
const y = item.y + 7;
|
|
1234
|
+
return `<text x="${(page.width / 2).toFixed(1)}" y="${y + 14}" text-anchor="middle" font-size="${SMALL_FONT_SIZE}" fill="#555555">${escapeXml(item.label)}</text>`;
|
|
1235
|
+
}
|
|
1236
|
+
|
|
1237
|
+
function renderTimeSvg(page, item) {
|
|
1238
|
+
const label = clipDisplay(item.label, 22);
|
|
1239
|
+
const labelWidth = Math.max(150, displayColumns(label) * 8 + 44);
|
|
1240
|
+
const x = page.width / 2 - labelWidth / 2;
|
|
1241
|
+
const y = item.y + 4;
|
|
1242
|
+
return [
|
|
1243
|
+
'<g class="time-row">',
|
|
1244
|
+
diamondSvg(x - 28, y + 16, 13, COMIC_THEME.timeAccentLeft),
|
|
1245
|
+
`<rect x="${(x + 3).toFixed(1)}" y="${y + 4}" width="${labelWidth}" height="30" rx="15" fill="${BLACK}"/>`,
|
|
1246
|
+
`<rect x="${x.toFixed(1)}" y="${y}" width="${labelWidth}" height="30" rx="15" fill="${COMIC_THEME.timeFill}" stroke="${BLACK}" stroke-width="3"/>`,
|
|
1247
|
+
`<text x="${(page.width / 2).toFixed(1)}" y="${y + 21}" text-anchor="middle" font-size="${FONT_SIZE}" font-weight="700" fill="${BLACK}">${escapeXml(label)}</text>`,
|
|
1248
|
+
diamondSvg(x + labelWidth + 28, y + 16, 13, COMIC_THEME.timeAccentRight),
|
|
1249
|
+
'</g>',
|
|
1250
|
+
].join('\n');
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1253
|
+
function renderMessageSvg(item) {
|
|
1254
|
+
const colors = sideColors(item.side);
|
|
1255
|
+
const messageText = item.message?.text || '';
|
|
1256
|
+
const titleText = escapeXml(`${item.participantLabel}: ${ellipsizeText(messageText, 42)}`);
|
|
1257
|
+
const parts = [
|
|
1258
|
+
`<g class="message-row ${item.side}" role="listitem" aria-label="${titleText}">`,
|
|
1259
|
+
`<title>${titleText}</title>`,
|
|
1260
|
+
bubbleLayersSvg(item, colors),
|
|
1261
|
+
`<rect x="${item.labelX}" y="${item.labelY}" width="${item.labelWidth}" height="${LABEL_HEIGHT}" rx="9" fill="${colors.label}" stroke="${BLACK}" stroke-width="3"/>`,
|
|
1262
|
+
`<text x="${(item.labelX + item.labelWidth / 2).toFixed(1)}" y="${item.labelY + 21}" text-anchor="middle" font-size="${LABEL_FONT_SIZE}" font-weight="900" fill="${BLACK}">${escapeXml(item.label)}</text>`,
|
|
1263
|
+
];
|
|
1264
|
+
const textX = item.bubbleX + BUBBLE_PAD_X;
|
|
1265
|
+
let textY = item.bubbleY + BUBBLE_PAD_Y + 17;
|
|
1266
|
+
for (const line of item.lines) {
|
|
1267
|
+
parts.push(`<text x="${textX}" y="${textY}" font-size="${FONT_SIZE}" font-weight="800" fill="${BLACK}">${escapeXml(line)}</text>`);
|
|
1268
|
+
textY += LINE_HEIGHT;
|
|
1269
|
+
}
|
|
1270
|
+
const tags = messageTags(item.message);
|
|
1271
|
+
if (tags.length) parts.push(renderTagIconsSvg(tags, textX, textY + TAG_ICON_TOP_GAP));
|
|
1272
|
+
parts.push('</g>');
|
|
1273
|
+
return parts.join('\n');
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1276
|
+
function bubbleLayersSvg(item, colors) {
|
|
1277
|
+
const x = item.bubbleX;
|
|
1278
|
+
const y = item.bubbleY;
|
|
1279
|
+
const width = item.width;
|
|
1280
|
+
const height = item.bubbleHeight;
|
|
1281
|
+
return [
|
|
1282
|
+
`<rect x="${x + 11}" y="${y + 9}" width="${width + 2}" height="${height + 4}" rx="17" fill="${BLACK}"/>`,
|
|
1283
|
+
`<rect x="${x + 11}" y="${y + 9}" width="${width - 3}" height="${height}" rx="17" fill="${colors.accent}" stroke="${BLACK}" stroke-width="3"/>`,
|
|
1284
|
+
`<rect x="${x}" y="${y}" width="${width}" height="${height}" rx="17" fill="${colors.fill}" stroke="${BLACK}" stroke-width="4"/>`,
|
|
1285
|
+
].join('\n');
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1288
|
+
function sideColors(side) {
|
|
1289
|
+
if (side === 'right') {
|
|
1290
|
+
return {
|
|
1291
|
+
fill: COMIC_THEME.rightFill,
|
|
1292
|
+
label: COMIC_THEME.rightLabel,
|
|
1293
|
+
accent: 'url(#rightAccent)',
|
|
1294
|
+
};
|
|
1295
|
+
}
|
|
1296
|
+
return {
|
|
1297
|
+
fill: COMIC_THEME.leftFill,
|
|
1298
|
+
label: COMIC_THEME.leftLabel,
|
|
1299
|
+
accent: 'url(#leftAccent)',
|
|
1300
|
+
};
|
|
1301
|
+
}
|
|
1302
|
+
|
|
1303
|
+
function headerTitle(title) {
|
|
1304
|
+
const clean = String(title || '').trim();
|
|
1305
|
+
if (clean.startsWith('@')) return clean;
|
|
1306
|
+
return clean ? `@${clean}` : '@claworld';
|
|
1307
|
+
}
|
|
1308
|
+
|
|
1309
|
+
function labelText(label) {
|
|
1310
|
+
return clipDisplay(String(label || 'AGENT').toUpperCase(), LABEL_MAX_COLS);
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1313
|
+
function labelWidthForText(label) {
|
|
1314
|
+
return Math.max(86, Math.min(158, displayColumns(label) * 11 + 30));
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1317
|
+
function tagRowUnits(tags) {
|
|
1318
|
+
if (!tags.length) return 0;
|
|
1319
|
+
const width = tags.reduce((sum, tag) => sum + tagWidth(tag), 0) + Math.max(0, tags.length - 1) * TAG_ICON_GAP;
|
|
1320
|
+
return width / TEXT_UNIT_PX;
|
|
1321
|
+
}
|
|
1322
|
+
|
|
1323
|
+
function renderTagIconsSvg(tags, x, y) {
|
|
1324
|
+
const parts = [`<g class="tag-icons" transform="translate(${x.toFixed(1)} ${y.toFixed(1)})">`];
|
|
1325
|
+
let cursor = 0;
|
|
1326
|
+
for (const tag of tags) {
|
|
1327
|
+
parts.push(tagIconSvg(tag, cursor, 0));
|
|
1328
|
+
cursor += tagWidth(tag) + TAG_ICON_GAP;
|
|
1329
|
+
}
|
|
1330
|
+
parts.push('</g>');
|
|
1331
|
+
return parts.join('\n');
|
|
1332
|
+
}
|
|
1333
|
+
|
|
1334
|
+
function tagWidth(tag) {
|
|
1335
|
+
const normalized = tagName(tag);
|
|
1336
|
+
if (['like', 'dislike', 'request end'].includes(normalized)) return TAG_ICON_SIZE;
|
|
1337
|
+
return Math.max(58, Math.min(126, displayColumns(fallbackTagLabel(normalized)) * 8 + 24));
|
|
1338
|
+
}
|
|
1339
|
+
|
|
1340
|
+
function tagIconSvg(tag, x, y) {
|
|
1341
|
+
const normalized = tagName(tag);
|
|
1342
|
+
if (!['like', 'dislike', 'request end'].includes(normalized)) return fallbackTagSvg(normalized, x, y);
|
|
1343
|
+
const [fill, accent] = TAG_ICON_THEMES[normalized] || ['#FFFFFF', '#7DD7FF'];
|
|
1344
|
+
const icon = normalized === 'request end'
|
|
1345
|
+
? requestEndIconSvg(x, y, accent)
|
|
1346
|
+
: thumbIconSvg(x, y, accent, { down: normalized === 'dislike' });
|
|
1347
|
+
return [
|
|
1348
|
+
`<g class="tag-icon tag-${safeClassToken(normalized)}" role="img" aria-label="${escapeXml(normalized)}">`,
|
|
1349
|
+
`<title>${escapeXml(normalized)}</title>`,
|
|
1350
|
+
`<rect x="${(x + 3).toFixed(1)}" y="${(y + 4).toFixed(1)}" width="${TAG_ICON_SIZE}" height="${TAG_ICON_SIZE}" rx="9" fill="${BLACK}"/>`,
|
|
1351
|
+
`<rect x="${x.toFixed(1)}" y="${y.toFixed(1)}" width="${TAG_ICON_SIZE}" height="${TAG_ICON_SIZE}" rx="9" fill="${fill}" stroke="${BLACK}" stroke-width="2.5"/>`,
|
|
1352
|
+
icon,
|
|
1353
|
+
'</g>',
|
|
1354
|
+
].join('\n');
|
|
1355
|
+
}
|
|
1356
|
+
|
|
1357
|
+
function tagName(tag) {
|
|
1358
|
+
return String(tag || '').trim().toLowerCase();
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1361
|
+
function fallbackTagLabel(tag) {
|
|
1362
|
+
return clipDisplay(String(tag || 'tag'), TAG_FALLBACK_MAX_COLS);
|
|
1363
|
+
}
|
|
1364
|
+
|
|
1365
|
+
function fallbackTagSvg(tag, x, y) {
|
|
1366
|
+
const label = fallbackTagLabel(tag);
|
|
1367
|
+
const width = tagWidth(tag);
|
|
1368
|
+
return [
|
|
1369
|
+
`<g class="tag-icon tag-fallback" role="img" aria-label="${escapeXml(label)}">`,
|
|
1370
|
+
`<title>${escapeXml(label)}</title>`,
|
|
1371
|
+
`<rect x="${(x + 3).toFixed(1)}" y="${(y + 4).toFixed(1)}" width="${width}" height="${TAG_ICON_SIZE}" rx="9" fill="${BLACK}"/>`,
|
|
1372
|
+
`<rect x="${x.toFixed(1)}" y="${y.toFixed(1)}" width="${width}" height="${TAG_ICON_SIZE}" rx="9" fill="#F6F1FF" stroke="${BLACK}" stroke-width="2.5"/>`,
|
|
1373
|
+
`<text x="${(x + width / 2).toFixed(1)}" y="${(y + 20.5).toFixed(1)}" text-anchor="middle" font-size="${LABEL_FONT_SIZE}" font-weight="900" fill="${BLACK}">${escapeXml(label)}</text>`,
|
|
1374
|
+
'</g>',
|
|
1375
|
+
].join('\n');
|
|
1376
|
+
}
|
|
1377
|
+
|
|
1378
|
+
function thumbIconSvg(x, y, accent, { down = false } = {}) {
|
|
1379
|
+
const paths = [
|
|
1380
|
+
`<path d="M${(x + 7.8).toFixed(1)} ${(y + 13.3).toFixed(1)} H${(x + 12).toFixed(1)} V${(y + 24).toFixed(1)} H${(x + 7.8).toFixed(1)} Z" fill="#FFFFFF" stroke="${BLACK}" stroke-width="2" stroke-linejoin="round"/>`,
|
|
1381
|
+
`<path d="M${(x + 12).toFixed(1)} ${(y + 23.6).toFixed(1)} H${(x + 21.2).toFixed(1)} C${(x + 23).toFixed(1)} ${(y + 23.6).toFixed(1)} ${(x + 24).toFixed(1)} ${(y + 22.5).toFixed(1)} ${(x + 24.4).toFixed(1)} ${(y + 20.9).toFixed(1)} L${(x + 25.4).toFixed(1)} ${(y + 15.6).toFixed(1)} C${(x + 25.8).toFixed(1)} ${(y + 13.9).toFixed(1)} ${(x + 24.5).toFixed(1)} ${(y + 12.2).toFixed(1)} ${(x + 22.7).toFixed(1)} ${(y + 12.2).toFixed(1)} H${(x + 18.6).toFixed(1)} L${(x + 19.2).toFixed(1)} ${(y + 9.1).toFixed(1)} C${(x + 19.5).toFixed(1)} ${(y + 7.4).toFixed(1)} ${(x + 18.4).toFixed(1)} ${(y + 5.7).toFixed(1)} ${(x + 16.7).toFixed(1)} ${(y + 5.4).toFixed(1)} L${(x + 15.8).toFixed(1)} ${(y + 5.3).toFixed(1)} L${(x + 12).toFixed(1)} ${(y + 12.9).toFixed(1)} Z" fill="${accent}" stroke="${BLACK}" stroke-width="2" stroke-linejoin="round"/>`,
|
|
1382
|
+
].join('\n');
|
|
1383
|
+
if (!down) return paths;
|
|
1384
|
+
const cx = x + TAG_ICON_SIZE / 2;
|
|
1385
|
+
const cy = y + TAG_ICON_SIZE / 2;
|
|
1386
|
+
return `<g transform="rotate(180 ${cx.toFixed(1)} ${cy.toFixed(1)})">\n${paths}\n</g>`;
|
|
1387
|
+
}
|
|
1388
|
+
|
|
1389
|
+
function requestEndIconSvg(x, y, accent) {
|
|
1390
|
+
return [
|
|
1391
|
+
`<path d="M${(x + 22).toFixed(1)} ${(y + 4).toFixed(1)} C${(x + 25.2).toFixed(1)} ${(y + 5.4).toFixed(1)} ${(x + 26.6).toFixed(1)} ${(y + 7.8).toFixed(1)} ${(x + 26.5).toFixed(1)} ${(y + 10.6).toFixed(1)}" fill="none" stroke="${BLACK}" stroke-width="2" stroke-linecap="round"/>`,
|
|
1392
|
+
`<path d="M${(x + 9.215).toFixed(3)} ${(y + 18.117).toFixed(3)} C${(x + 10.043).toFixed(3)} ${(y + 22.457).toFixed(3)} ${(x + 13.384).toFixed(3)} ${(y + 25.036).toFixed(3)} ${(x + 17.132).toFixed(3)} ${(y + 23.961).toFixed(3)} L${(x + 19.44).toFixed(3)} ${(y + 23.3).toFixed(3)} C${(x + 22.323).toFixed(3)} ${(y + 22.473).toFixed(3)} ${(x + 23.488).toFixed(3)} ${(y + 19.642).toFixed(3)} ${(x + 22.538).toFixed(3)} ${(y + 16.69).toFixed(3)} L${(x + 21.435).toFixed(3)} ${(y + 12.845).toFixed(3)} L${(x + 9.227).toFixed(3)} ${(y + 16.345).toFixed(3)} L${(x + 9.215).toFixed(3)} ${(y + 18.117).toFixed(3)} Z" fill="${accent}" stroke="${BLACK}" stroke-width="2" stroke-linejoin="round"/>`,
|
|
1393
|
+
`<path d="M${(x + 9.665).toFixed(3)} ${(y + 7.897).toFixed(3)} L${(x + 9.473).toFixed(3)} ${(y + 7.952).toFixed(3)} C${(x + 8.756).toFixed(3)} ${(y + 8.158).toFixed(3)} ${(x + 8.286).toFixed(3)} ${(y + 8.709).toFixed(3)} ${(x + 8.422).toFixed(3)} ${(y + 9.184).toFixed(3)} L${(x + 10.192).toFixed(3)} ${(y + 15.356).toFixed(3)} C${(x + 10.328).toFixed(3)} ${(y + 15.831).toFixed(3)} ${(x + 11.019).toFixed(3)} ${(y + 16.049).toFixed(3)} ${(x + 11.736).toFixed(3)} ${(y + 15.843).toFixed(3)} L${(x + 11.928).toFixed(3)} ${(y + 15.788).toFixed(3)} C${(x + 12.645).toFixed(3)} ${(y + 15.583).toFixed(3)} ${(x + 13.115).toFixed(3)} ${(y + 15.031).toFixed(3)} ${(x + 12.979).toFixed(3)} ${(y + 14.557).toFixed(3)} L${(x + 11.209).toFixed(3)} ${(y + 8.384).toFixed(3)} C${(x + 11.073).toFixed(3)} ${(y + 7.91).toFixed(3)} ${(x + 10.382).toFixed(3)} ${(y + 7.692).toFixed(3)} ${(x + 9.665).toFixed(3)} ${(y + 7.897).toFixed(3)} Z" fill="${accent}" stroke="${BLACK}" stroke-width="1.8"/>`,
|
|
1394
|
+
`<path d="M${(x + 11.93).toFixed(3)} ${(y + 5.999).toFixed(3)} L${(x + 11.738).toFixed(3)} ${(y + 6.055).toFixed(3)} C${(x + 11.021).toFixed(3)} ${(y + 6.26).toFixed(3)} ${(x + 10.553).toFixed(3)} ${(y + 6.82).toFixed(3)} ${(x + 10.692).toFixed(3)} ${(y + 7.306).toFixed(3)} L${(x + 12.729).toFixed(3)} ${(y + 14.408).toFixed(3)} C${(x + 12.868).toFixed(3)} ${(y + 14.894).toFixed(3)} ${(x + 13.562).toFixed(3)} ${(y + 15.122).toFixed(3)} ${(x + 14.279).toFixed(3)} ${(y + 14.916).toFixed(3)} L${(x + 14.471).toFixed(3)} ${(y + 14.861).toFixed(3)} C${(x + 15.188).toFixed(3)} ${(y + 14.655).toFixed(3)} ${(x + 15.656).toFixed(3)} ${(y + 14.095).toFixed(3)} ${(x + 15.516).toFixed(3)} ${(y + 13.609).toFixed(3)} L${(x + 13.48).toFixed(3)} ${(y + 6.507).toFixed(3)} C${(x + 13.341).toFixed(3)} ${(y + 6.021).toFixed(3)} ${(x + 12.647).toFixed(3)} ${(y + 5.794).toFixed(3)} ${(x + 11.93).toFixed(3)} ${(y + 5.999).toFixed(3)} Z" fill="${accent}" stroke="${BLACK}" stroke-width="1.8"/>`,
|
|
1395
|
+
`<path d="M${(x + 14.636).toFixed(3)} ${(y + 5.64).toFixed(3)} L${(x + 14.443).toFixed(3)} ${(y + 5.695).toFixed(3)} C${(x + 13.727).toFixed(3)} ${(y + 5.9).toFixed(3)} ${(x + 13.258).toFixed(3)} ${(y + 6.457).toFixed(3)} ${(x + 13.396).toFixed(3)} ${(y + 6.938).toFixed(3)} L${(x + 15.338).toFixed(3)} ${(y + 13.714).toFixed(3)} C${(x + 15.476).toFixed(3)} ${(y + 14.195).toFixed(3)} ${(x + 16.169).toFixed(3)} ${(y + 14.418).toFixed(3)} ${(x + 16.886).toFixed(3)} ${(y + 14.213).toFixed(3)} L${(x + 17.078).toFixed(3)} ${(y + 14.157).toFixed(3)} C${(x + 17.795).toFixed(3)} ${(y + 13.952).toFixed(3)} ${(x + 18.264).toFixed(3)} ${(y + 13.395).toFixed(3)} ${(x + 18.126).toFixed(3)} ${(y + 12.914).toFixed(3)} L${(x + 16.183).toFixed(3)} ${(y + 6.139).toFixed(3)} C${(x + 16.045).toFixed(3)} ${(y + 5.658).toFixed(3)} ${(x + 15.352).toFixed(3)} ${(y + 5.434).toFixed(3)} ${(x + 14.636).toFixed(3)} ${(y + 5.64).toFixed(3)} Z" fill="${accent}" stroke="${BLACK}" stroke-width="1.8"/>`,
|
|
1396
|
+
`<path d="M${(x + 17.8).toFixed(3)} ${(y + 6.085).toFixed(3)} L${(x + 17.702).toFixed(3)} ${(y + 6.113).toFixed(3)} C${(x + 16.971).toFixed(3)} ${(y + 6.322).toFixed(3)} ${(x + 16.482).toFixed(3)} ${(y + 6.851).toFixed(3)} ${(x + 16.609).toFixed(3)} ${(y + 7.294).toFixed(3)} L${(x + 18.176).toFixed(3)} ${(y + 12.759).toFixed(3)} C${(x + 18.303).toFixed(3)} ${(y + 13.202).toFixed(3)} ${(x + 18.998).toFixed(3)} ${(y + 13.391).toFixed(3)} ${(x + 19.729).toFixed(3)} ${(y + 13.182).toFixed(3)} L${(x + 19.827).toFixed(3)} ${(y + 13.154).toFixed(3)} C${(x + 20.557).toFixed(3)} ${(y + 12.944).toFixed(3)} ${(x + 21.046).toFixed(3)} ${(y + 12.415).toFixed(3)} ${(x + 20.919).toFixed(3)} ${(y + 11.972).toFixed(3)} L${(x + 19.352).toFixed(3)} ${(y + 6.507).toFixed(3)} C${(x + 19.225).toFixed(3)} ${(y + 6.065).toFixed(3)} ${(x + 18.53).toFixed(3)} ${(y + 5.875).toFixed(3)} ${(x + 17.8).toFixed(3)} ${(y + 6.085).toFixed(3)} Z" fill="${accent}" stroke="${BLACK}" stroke-width="1.8"/>`,
|
|
1397
|
+
`<path d="M${(x + 9.546).toFixed(3)} ${(y + 19.999).toFixed(3)} L${(x + 6).toFixed(3)} ${(y + 17.791).toFixed(3)} C${(x + 4.736).toFixed(3)} ${(y + 17.009).toFixed(3)} ${(x + 5.668).toFixed(3)} ${(y + 15.181).toFixed(3)} ${(x + 7.138).toFixed(3)} ${(y + 15.592).toFixed(3)} L${(x + 10.615).toFixed(3)} ${(y + 17.196).toFixed(3)} L${(x + 9.546).toFixed(3)} ${(y + 19.999).toFixed(3)} Z" fill="${accent}" stroke="${BLACK}" stroke-width="1.8" stroke-linejoin="round"/>`,
|
|
1398
|
+
`<rect x="${(x + 10.887).toFixed(3)}" y="${(y + 14.834).toFixed(3)}" width="9.584" height="3.800" transform="rotate(-16.9438 ${(x + 10.887).toFixed(3)} ${(y + 14.834).toFixed(3)})" fill="${accent}"/>`,
|
|
1399
|
+
`<path d="M${(x + 10.5).toFixed(1)} ${(y + 19.5).toFixed(1)} L${(x + 11).toFixed(1)} ${(y + 20).toFixed(1)} L${(x + 11.5).toFixed(1)} ${(y + 17).toFixed(1)} L${(x + 9.5).toFixed(1)} ${(y + 17.5).toFixed(1)} L${(x + 9).toFixed(1)} ${(y + 18.5).toFixed(1)} L${(x + 10.5).toFixed(1)} ${(y + 19.5).toFixed(1)} Z" fill="${accent}"/>`,
|
|
1400
|
+
].join('\n');
|
|
1401
|
+
}
|
|
1402
|
+
|
|
1403
|
+
function decorativeStarSvg(cx, cy, radius, fill, accent) {
|
|
1404
|
+
const backPath = starPoints(cx + 3, cy + 5, radius).map(([x, y]) => `${x.toFixed(1)},${y.toFixed(1)}`).join(' ');
|
|
1405
|
+
const frontPath = starPoints(cx, cy, radius).map(([x, y]) => `${x.toFixed(1)},${y.toFixed(1)}`).join(' ');
|
|
1406
|
+
return [
|
|
1407
|
+
`<polygon points="${backPath}" fill="${accent}"/>`,
|
|
1408
|
+
`<polygon points="${frontPath}" fill="${fill}" stroke="${BLACK}" stroke-width="3"/>`,
|
|
1409
|
+
].join('\n');
|
|
1410
|
+
}
|
|
1411
|
+
|
|
1412
|
+
function diamondSvg(cx, cy, radius, fill) {
|
|
1413
|
+
const shadow = `<polygon points="${(cx + 1).toFixed(1)},${(cy + 2 - radius).toFixed(1)} ${(cx + 1 + radius * 0.46).toFixed(1)},${(cy + 2).toFixed(1)} ${(cx + 1).toFixed(1)},${(cy + 2 + radius).toFixed(1)} ${(cx + 1 - radius * 0.46).toFixed(1)},${(cy + 2).toFixed(1)}" fill="${BLACK}" stroke="${BLACK}" stroke-width="2.5"/>`;
|
|
1414
|
+
const front = `<polygon points="${cx.toFixed(1)},${(cy - radius).toFixed(1)} ${(cx + radius * 0.46).toFixed(1)},${cy.toFixed(1)} ${cx.toFixed(1)},${(cy + radius).toFixed(1)} ${(cx - radius * 0.46).toFixed(1)},${cy.toFixed(1)}" fill="${fill}" stroke="${BLACK}" stroke-width="2.5"/>`;
|
|
1415
|
+
return `${shadow}\n${front}`;
|
|
1416
|
+
}
|
|
1417
|
+
|
|
1418
|
+
function starPoints(cx, cy, radius) {
|
|
1419
|
+
return [
|
|
1420
|
+
[cx, cy - radius],
|
|
1421
|
+
[cx + radius * 0.28, cy - radius * 0.28],
|
|
1422
|
+
[cx + radius, cy],
|
|
1423
|
+
[cx + radius * 0.28, cy + radius * 0.28],
|
|
1424
|
+
[cx, cy + radius],
|
|
1425
|
+
[cx - radius * 0.28, cy + radius * 0.28],
|
|
1426
|
+
[cx - radius, cy],
|
|
1427
|
+
[cx - radius * 0.28, cy - radius * 0.28],
|
|
1428
|
+
];
|
|
1429
|
+
}
|
|
1430
|
+
|
|
1431
|
+
function safeClassToken(value) {
|
|
1432
|
+
return String(value || 'tag').toLowerCase().replace(/[^a-z0-9_-]+/g, '-') || 'tag';
|
|
1433
|
+
}
|
|
1434
|
+
|
|
1435
|
+
function svgDefs() {
|
|
1436
|
+
return [
|
|
1437
|
+
'<defs>',
|
|
1438
|
+
`<style><![CDATA[text { font-family: ${COMIC_FONT_FAMILY}; letter-spacing: 0; } .message-row:hover rect:last-of-type { filter: url(#comicLift); }]]></style>`,
|
|
1439
|
+
'<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>',
|
|
1440
|
+
'<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>',
|
|
1441
|
+
'<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>',
|
|
1442
|
+
'<linearGradient id="leftAccent" x1="0" y1="0" x2="1" y2="0"><stop offset="0%" stop-color="#58E58F"/><stop offset="100%" stop-color="#47B6FF"/></linearGradient>',
|
|
1443
|
+
'<linearGradient id="rightAccent" x1="0" y1="0" x2="1" y2="0"><stop offset="0%" stop-color="#A871FF"/><stop offset="100%" stop-color="#FF4EB4"/></linearGradient>',
|
|
1444
|
+
'<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>',
|
|
1445
|
+
'</defs>',
|
|
1446
|
+
].join('\n');
|
|
1447
|
+
}
|
|
1448
|
+
|
|
881
1449
|
function buildBubbleSpec({ mode, source, header, messages, layout, style }) {
|
|
882
1450
|
return {
|
|
883
1451
|
schema: ARTIFACT_SCHEMA,
|
|
@@ -1043,11 +1611,13 @@ async function writeArtifacts({ workspaceRoot, artifactId, spec, layout }) {
|
|
|
1043
1611
|
page: page.page,
|
|
1044
1612
|
format: 'png',
|
|
1045
1613
|
path: pngPath,
|
|
1614
|
+
filePath: pngPath,
|
|
1615
|
+
filename: path.basename(pngPath),
|
|
1616
|
+
mimeType: 'image/png',
|
|
1046
1617
|
width: page.width,
|
|
1047
1618
|
height: page.height,
|
|
1048
1619
|
renderer,
|
|
1049
1620
|
sha256: await hashFile(pngPath),
|
|
1050
|
-
mediaRef: `MEDIA:${pngPath}`,
|
|
1051
1621
|
});
|
|
1052
1622
|
}
|
|
1053
1623
|
return {
|
|
@@ -1061,6 +1631,19 @@ async function writeArtifacts({ workspaceRoot, artifactId, spec, layout }) {
|
|
|
1061
1631
|
};
|
|
1062
1632
|
}
|
|
1063
1633
|
|
|
1634
|
+
function buildOpenClawUploadArgs(page, message = null) {
|
|
1635
|
+
if (!page?.path) return null;
|
|
1636
|
+
return {
|
|
1637
|
+
action: 'upload-file',
|
|
1638
|
+
filePath: page.path,
|
|
1639
|
+
path: page.path,
|
|
1640
|
+
media: page.path,
|
|
1641
|
+
filename: page.filename || path.basename(page.path),
|
|
1642
|
+
mimeType: page.mimeType || 'image/png',
|
|
1643
|
+
...(message ? { message } : {}),
|
|
1644
|
+
};
|
|
1645
|
+
}
|
|
1646
|
+
|
|
1064
1647
|
function buildDiagnostics({ sourceRecords, normalizedMessages, renderedMessages, source }) {
|
|
1065
1648
|
return {
|
|
1066
1649
|
source: {
|
|
@@ -1168,10 +1751,16 @@ export async function renderClaworldTranscriptReport({
|
|
|
1168
1751
|
style: params.style,
|
|
1169
1752
|
artifacts,
|
|
1170
1753
|
deliveryHint: {
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1754
|
+
primaryFilePath: artifacts.pngPages[0]?.path || null,
|
|
1755
|
+
imageFilePaths: artifacts.pngPages.map((page) => page.path),
|
|
1756
|
+
sourceSvgFilePaths: artifacts.svgPages.map((page) => page.path),
|
|
1757
|
+
messageTool: buildOpenClawUploadArgs(
|
|
1758
|
+
artifacts.pngPages[0],
|
|
1759
|
+
source.chatRequestId
|
|
1760
|
+
? `Claworld transcript report for ${source.chatRequestId}`
|
|
1761
|
+
: 'Claworld transcript report',
|
|
1762
|
+
),
|
|
1763
|
+
messageToolBatch: artifacts.pngPages.map((page) => buildOpenClawUploadArgs(page)).filter(Boolean),
|
|
1175
1764
|
},
|
|
1176
1765
|
diagnostics: buildDiagnostics({
|
|
1177
1766
|
sourceRecords,
|