@oh-my-pi/pi-web-ui 3.13.1337 → 3.15.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.
- package/CHANGELOG.md +8 -0
- package/package.json +4 -4
- package/src/utils/attachment-utils.ts +151 -17
- package/src/utils/test-sessions.ts +0 -33
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [3.15.0] - 2026-01-05
|
|
6
|
+
|
|
7
|
+
### Changed
|
|
8
|
+
|
|
9
|
+
- Resize large image attachments before sending (max 1920x1080 if dimension exceeds 2048) and convert >2MB images to JPEG
|
|
10
|
+
|
|
11
|
+
## [3.14.0] - 2026-01-04
|
|
12
|
+
|
|
5
13
|
## [3.13.1337] - 2026-01-04
|
|
6
14
|
|
|
7
15
|
## [3.9.1337] - 2026-01-04
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oh-my-pi/pi-web-ui",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.15.0",
|
|
4
4
|
"description": "Reusable web UI components for AI chat interfaces powered by @oh-my-pi/pi-ai",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.ts",
|
|
@@ -19,9 +19,9 @@
|
|
|
19
19
|
},
|
|
20
20
|
"dependencies": {
|
|
21
21
|
"@lmstudio/sdk": "^1.5.0",
|
|
22
|
-
"@oh-my-pi/pi-agent-core": "3.
|
|
23
|
-
"@oh-my-pi/pi-ai": "3.
|
|
24
|
-
"@oh-my-pi/pi-tui": "3.
|
|
22
|
+
"@oh-my-pi/pi-agent-core": "3.15.0",
|
|
23
|
+
"@oh-my-pi/pi-ai": "3.15.0",
|
|
24
|
+
"@oh-my-pi/pi-tui": "3.15.0",
|
|
25
25
|
"docx-preview": "^0.3.7",
|
|
26
26
|
"highlight.js": "^11.11.1",
|
|
27
27
|
"jszip": "^3.10.1",
|
|
@@ -19,6 +19,133 @@ export interface Attachment {
|
|
|
19
19
|
preview?: string; // base64 image preview (first page for PDFs, or same as content for images)
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
+
const RESIZE_TRIGGER_MAX_DIMENSION = 2048;
|
|
23
|
+
const MAX_RESIZE_WIDTH = 1920;
|
|
24
|
+
const MAX_RESIZE_HEIGHT = 1080;
|
|
25
|
+
const JPEG_CONVERT_THRESHOLD_BYTES = 2 * 1024 * 1024;
|
|
26
|
+
const JPEG_QUALITY = 0.85;
|
|
27
|
+
|
|
28
|
+
function arrayBufferToBase64(arrayBuffer: ArrayBuffer): string {
|
|
29
|
+
const uint8Array = new Uint8Array(arrayBuffer);
|
|
30
|
+
let binary = "";
|
|
31
|
+
const chunkSize = 0x8000;
|
|
32
|
+
for (let i = 0; i < uint8Array.length; i += chunkSize) {
|
|
33
|
+
const chunk = uint8Array.slice(i, i + chunkSize);
|
|
34
|
+
binary += String.fromCharCode(...chunk);
|
|
35
|
+
}
|
|
36
|
+
return btoa(binary);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function getResizedDimensions(width: number, height: number): { width: number; height: number } {
|
|
40
|
+
const maxDim = Math.max(width, height);
|
|
41
|
+
if (maxDim <= RESIZE_TRIGGER_MAX_DIMENSION) {
|
|
42
|
+
return { width, height };
|
|
43
|
+
}
|
|
44
|
+
const scale = Math.min(MAX_RESIZE_WIDTH / width, MAX_RESIZE_HEIGHT / height);
|
|
45
|
+
return {
|
|
46
|
+
width: Math.max(1, Math.round(width * scale)),
|
|
47
|
+
height: Math.max(1, Math.round(height * scale)),
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function updateFileNameForJpeg(fileName: string): string {
|
|
52
|
+
const lower = fileName.toLowerCase();
|
|
53
|
+
if (lower.endsWith(".jpg") || lower.endsWith(".jpeg")) {
|
|
54
|
+
return fileName;
|
|
55
|
+
}
|
|
56
|
+
const dotIndex = fileName.lastIndexOf(".");
|
|
57
|
+
if (dotIndex > 0) {
|
|
58
|
+
return `${fileName.slice(0, dotIndex)}.jpg`;
|
|
59
|
+
}
|
|
60
|
+
return `${fileName}.jpg`;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async function decodeImage(arrayBuffer: ArrayBuffer, mimeType: string) {
|
|
64
|
+
const blob = new Blob([arrayBuffer], { type: mimeType });
|
|
65
|
+
if (typeof createImageBitmap === "function") {
|
|
66
|
+
const bitmap = await createImageBitmap(blob);
|
|
67
|
+
return {
|
|
68
|
+
width: bitmap.width,
|
|
69
|
+
height: bitmap.height,
|
|
70
|
+
draw: (ctx: CanvasRenderingContext2D, width: number, height: number) => {
|
|
71
|
+
ctx.drawImage(bitmap, 0, 0, width, height);
|
|
72
|
+
bitmap.close?.();
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const url = URL.createObjectURL(blob);
|
|
78
|
+
try {
|
|
79
|
+
const img = new Image();
|
|
80
|
+
img.src = url;
|
|
81
|
+
await img.decode();
|
|
82
|
+
return {
|
|
83
|
+
width: img.naturalWidth,
|
|
84
|
+
height: img.naturalHeight,
|
|
85
|
+
draw: (ctx: CanvasRenderingContext2D, width: number, height: number) => {
|
|
86
|
+
ctx.drawImage(img, 0, 0, width, height);
|
|
87
|
+
},
|
|
88
|
+
};
|
|
89
|
+
} finally {
|
|
90
|
+
URL.revokeObjectURL(url);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async function processImageAttachment(
|
|
95
|
+
arrayBuffer: ArrayBuffer,
|
|
96
|
+
fileName: string,
|
|
97
|
+
mimeType: string,
|
|
98
|
+
): Promise<{ base64: string; mimeType: string; size: number; fileName: string }> {
|
|
99
|
+
const shouldConvertToJpeg = arrayBuffer.byteLength > JPEG_CONVERT_THRESHOLD_BYTES;
|
|
100
|
+
const outputMimeType = shouldConvertToJpeg ? "image/jpeg" : mimeType;
|
|
101
|
+
|
|
102
|
+
const { width, height, draw } = await decodeImage(arrayBuffer, mimeType);
|
|
103
|
+
const resized = getResizedDimensions(width, height);
|
|
104
|
+
const shouldResize = resized.width !== width || resized.height !== height;
|
|
105
|
+
|
|
106
|
+
if (!shouldResize && !shouldConvertToJpeg) {
|
|
107
|
+
return {
|
|
108
|
+
base64: arrayBufferToBase64(arrayBuffer),
|
|
109
|
+
mimeType,
|
|
110
|
+
size: arrayBuffer.byteLength,
|
|
111
|
+
fileName,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const canvas = document.createElement("canvas");
|
|
116
|
+
canvas.width = resized.width;
|
|
117
|
+
canvas.height = resized.height;
|
|
118
|
+
const ctx = canvas.getContext("2d");
|
|
119
|
+
if (!ctx) {
|
|
120
|
+
throw new Error("Failed to create canvas context");
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (outputMimeType === "image/jpeg") {
|
|
124
|
+
ctx.fillStyle = "#ffffff";
|
|
125
|
+
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
|
126
|
+
}
|
|
127
|
+
draw(ctx, resized.width, resized.height);
|
|
128
|
+
|
|
129
|
+
const blob = await new Promise<Blob>((resolve, reject) => {
|
|
130
|
+
canvas.toBlob(
|
|
131
|
+
(result) => {
|
|
132
|
+
if (result) resolve(result);
|
|
133
|
+
else reject(new Error("Failed to encode image"));
|
|
134
|
+
},
|
|
135
|
+
outputMimeType,
|
|
136
|
+
outputMimeType === "image/jpeg" ? JPEG_QUALITY : undefined,
|
|
137
|
+
);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
const outputBuffer = await blob.arrayBuffer();
|
|
141
|
+
return {
|
|
142
|
+
base64: arrayBufferToBase64(outputBuffer),
|
|
143
|
+
mimeType: outputMimeType,
|
|
144
|
+
size: blob.size,
|
|
145
|
+
fileName: shouldConvertToJpeg ? updateFileNameForJpeg(fileName) : fileName,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
22
149
|
/**
|
|
23
150
|
* Load an attachment from various sources
|
|
24
151
|
* @param source - URL string, File, Blob, or ArrayBuffer
|
|
@@ -67,14 +194,7 @@ export async function loadAttachment(
|
|
|
67
194
|
}
|
|
68
195
|
|
|
69
196
|
// Convert ArrayBuffer to base64 - handle large files properly
|
|
70
|
-
const
|
|
71
|
-
let binary = "";
|
|
72
|
-
const chunkSize = 0x8000; // Process in 32KB chunks to avoid stack overflow
|
|
73
|
-
for (let i = 0; i < uint8Array.length; i += chunkSize) {
|
|
74
|
-
const chunk = uint8Array.slice(i, i + chunkSize);
|
|
75
|
-
binary += String.fromCharCode(...chunk);
|
|
76
|
-
}
|
|
77
|
-
const base64Content = btoa(binary);
|
|
197
|
+
const base64Content = arrayBufferToBase64(arrayBuffer);
|
|
78
198
|
|
|
79
199
|
// Detect type and process accordingly
|
|
80
200
|
const id = `${detectedFileName}_${Date.now()}_${Math.random()}`;
|
|
@@ -154,15 +274,29 @@ export async function loadAttachment(
|
|
|
154
274
|
|
|
155
275
|
// Check if it's an image
|
|
156
276
|
if (mimeType.startsWith("image/")) {
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
277
|
+
try {
|
|
278
|
+
const processed = await processImageAttachment(arrayBuffer, detectedFileName, mimeType);
|
|
279
|
+
return {
|
|
280
|
+
id,
|
|
281
|
+
type: "image",
|
|
282
|
+
fileName: processed.fileName,
|
|
283
|
+
mimeType: processed.mimeType,
|
|
284
|
+
size: processed.size,
|
|
285
|
+
content: processed.base64,
|
|
286
|
+
preview: processed.base64, // For images, preview is the same as content
|
|
287
|
+
};
|
|
288
|
+
} catch (error) {
|
|
289
|
+
console.error("Error processing image:", error);
|
|
290
|
+
return {
|
|
291
|
+
id,
|
|
292
|
+
type: "image",
|
|
293
|
+
fileName: detectedFileName,
|
|
294
|
+
mimeType,
|
|
295
|
+
size,
|
|
296
|
+
content: base64Content,
|
|
297
|
+
preview: base64Content, // For images, preview is the same as content
|
|
298
|
+
};
|
|
299
|
+
}
|
|
166
300
|
}
|
|
167
301
|
|
|
168
302
|
// Check if it's a text document
|
|
@@ -73,7 +73,6 @@ export const simpleHtml = {
|
|
|
73
73
|
toolCallId: "toolu_01Tu6wbnPMHtBKj9B7TMos1x",
|
|
74
74
|
toolName: "artifacts",
|
|
75
75
|
output: "Created file index.html",
|
|
76
|
-
isError: false,
|
|
77
76
|
},
|
|
78
77
|
{
|
|
79
78
|
role: "assistant",
|
|
@@ -180,7 +179,6 @@ export const longSession = {
|
|
|
180
179
|
toolCallId: "toolu_01Y3hvzepDjUWnHF8bdmgMSA",
|
|
181
180
|
toolName: "artifacts",
|
|
182
181
|
output: "Created file index.html",
|
|
183
|
-
isError: false,
|
|
184
182
|
},
|
|
185
183
|
{
|
|
186
184
|
role: "assistant",
|
|
@@ -264,7 +262,6 @@ export const longSession = {
|
|
|
264
262
|
details: {
|
|
265
263
|
files: [],
|
|
266
264
|
},
|
|
267
|
-
isError: false,
|
|
268
265
|
},
|
|
269
266
|
{
|
|
270
267
|
role: "assistant",
|
|
@@ -344,7 +341,6 @@ export const longSession = {
|
|
|
344
341
|
details: {
|
|
345
342
|
files: [],
|
|
346
343
|
},
|
|
347
|
-
isError: false,
|
|
348
344
|
},
|
|
349
345
|
{
|
|
350
346
|
role: "assistant",
|
|
@@ -428,7 +424,6 @@ export const longSession = {
|
|
|
428
424
|
details: {
|
|
429
425
|
files: [],
|
|
430
426
|
},
|
|
431
|
-
isError: false,
|
|
432
427
|
},
|
|
433
428
|
{
|
|
434
429
|
role: "assistant",
|
|
@@ -512,7 +507,6 @@ export const longSession = {
|
|
|
512
507
|
details: {
|
|
513
508
|
files: [],
|
|
514
509
|
},
|
|
515
|
-
isError: false,
|
|
516
510
|
},
|
|
517
511
|
{
|
|
518
512
|
role: "assistant",
|
|
@@ -558,7 +552,6 @@ export const longSession = {
|
|
|
558
552
|
details: {
|
|
559
553
|
files: [],
|
|
560
554
|
},
|
|
561
|
-
isError: false,
|
|
562
555
|
},
|
|
563
556
|
{
|
|
564
557
|
role: "assistant",
|
|
@@ -676,7 +669,6 @@ export const longSession = {
|
|
|
676
669
|
details: {
|
|
677
670
|
files: [],
|
|
678
671
|
},
|
|
679
|
-
isError: false,
|
|
680
672
|
},
|
|
681
673
|
{
|
|
682
674
|
role: "assistant",
|
|
@@ -723,7 +715,6 @@ export const longSession = {
|
|
|
723
715
|
details: {
|
|
724
716
|
files: [],
|
|
725
717
|
},
|
|
726
|
-
isError: false,
|
|
727
718
|
},
|
|
728
719
|
{
|
|
729
720
|
role: "assistant",
|
|
@@ -806,7 +797,6 @@ export const longSession = {
|
|
|
806
797
|
details: {
|
|
807
798
|
files: [],
|
|
808
799
|
},
|
|
809
|
-
isError: false,
|
|
810
800
|
},
|
|
811
801
|
{
|
|
812
802
|
role: "assistant",
|
|
@@ -889,7 +879,6 @@ export const longSession = {
|
|
|
889
879
|
details: {
|
|
890
880
|
files: [],
|
|
891
881
|
},
|
|
892
|
-
isError: false,
|
|
893
882
|
},
|
|
894
883
|
{
|
|
895
884
|
role: "assistant",
|
|
@@ -1002,7 +991,6 @@ export const longSession = {
|
|
|
1002
991
|
details: {
|
|
1003
992
|
files: [],
|
|
1004
993
|
},
|
|
1005
|
-
isError: false,
|
|
1006
994
|
},
|
|
1007
995
|
{
|
|
1008
996
|
role: "assistant",
|
|
@@ -1085,7 +1073,6 @@ export const longSession = {
|
|
|
1085
1073
|
details: {
|
|
1086
1074
|
files: [],
|
|
1087
1075
|
},
|
|
1088
|
-
isError: false,
|
|
1089
1076
|
},
|
|
1090
1077
|
{
|
|
1091
1078
|
role: "assistant",
|
|
@@ -1161,7 +1148,6 @@ export const longSession = {
|
|
|
1161
1148
|
details: {
|
|
1162
1149
|
files: [],
|
|
1163
1150
|
},
|
|
1164
|
-
isError: false,
|
|
1165
1151
|
},
|
|
1166
1152
|
{
|
|
1167
1153
|
role: "assistant",
|
|
@@ -1208,7 +1194,6 @@ export const longSession = {
|
|
|
1208
1194
|
details: {
|
|
1209
1195
|
files: [],
|
|
1210
1196
|
},
|
|
1211
|
-
isError: false,
|
|
1212
1197
|
},
|
|
1213
1198
|
{
|
|
1214
1199
|
role: "assistant",
|
|
@@ -1292,7 +1277,6 @@ export const longSession = {
|
|
|
1292
1277
|
details: {
|
|
1293
1278
|
files: [],
|
|
1294
1279
|
},
|
|
1295
|
-
isError: false,
|
|
1296
1280
|
},
|
|
1297
1281
|
{
|
|
1298
1282
|
role: "assistant",
|
|
@@ -1339,7 +1323,6 @@ export const longSession = {
|
|
|
1339
1323
|
details: {
|
|
1340
1324
|
files: [],
|
|
1341
1325
|
},
|
|
1342
|
-
isError: false,
|
|
1343
1326
|
},
|
|
1344
1327
|
{
|
|
1345
1328
|
role: "assistant",
|
|
@@ -1419,7 +1402,6 @@ export const longSession = {
|
|
|
1419
1402
|
details: {
|
|
1420
1403
|
files: [],
|
|
1421
1404
|
},
|
|
1422
|
-
isError: false,
|
|
1423
1405
|
},
|
|
1424
1406
|
{
|
|
1425
1407
|
role: "assistant",
|
|
@@ -1536,7 +1518,6 @@ export const longSession = {
|
|
|
1536
1518
|
details: {
|
|
1537
1519
|
files: [],
|
|
1538
1520
|
},
|
|
1539
|
-
isError: false,
|
|
1540
1521
|
},
|
|
1541
1522
|
{
|
|
1542
1523
|
role: "assistant",
|
|
@@ -1583,7 +1564,6 @@ export const longSession = {
|
|
|
1583
1564
|
details: {
|
|
1584
1565
|
files: [],
|
|
1585
1566
|
},
|
|
1586
|
-
isError: false,
|
|
1587
1567
|
},
|
|
1588
1568
|
{
|
|
1589
1569
|
role: "assistant",
|
|
@@ -1625,7 +1605,6 @@ export const longSession = {
|
|
|
1625
1605
|
details: {
|
|
1626
1606
|
files: [],
|
|
1627
1607
|
},
|
|
1628
|
-
isError: false,
|
|
1629
1608
|
},
|
|
1630
1609
|
{
|
|
1631
1610
|
role: "assistant",
|
|
@@ -1667,7 +1646,6 @@ export const longSession = {
|
|
|
1667
1646
|
details: {
|
|
1668
1647
|
files: [],
|
|
1669
1648
|
},
|
|
1670
|
-
isError: false,
|
|
1671
1649
|
},
|
|
1672
1650
|
{
|
|
1673
1651
|
role: "assistant",
|
|
@@ -1714,7 +1692,6 @@ export const longSession = {
|
|
|
1714
1692
|
details: {
|
|
1715
1693
|
files: [],
|
|
1716
1694
|
},
|
|
1717
|
-
isError: false,
|
|
1718
1695
|
},
|
|
1719
1696
|
{
|
|
1720
1697
|
role: "assistant",
|
|
@@ -1761,7 +1738,6 @@ export const longSession = {
|
|
|
1761
1738
|
toolCallId: "toolu_01BtH9H2BvwxvKjLw5iHXcZC",
|
|
1762
1739
|
toolName: "artifacts",
|
|
1763
1740
|
output: "Created file news_today.md",
|
|
1764
|
-
isError: false,
|
|
1765
1741
|
},
|
|
1766
1742
|
{
|
|
1767
1743
|
role: "assistant",
|
|
@@ -1845,7 +1821,6 @@ export const longSession = {
|
|
|
1845
1821
|
details: {
|
|
1846
1822
|
files: [],
|
|
1847
1823
|
},
|
|
1848
|
-
isError: false,
|
|
1849
1824
|
},
|
|
1850
1825
|
{
|
|
1851
1826
|
role: "assistant",
|
|
@@ -1892,7 +1867,6 @@ export const longSession = {
|
|
|
1892
1867
|
details: {
|
|
1893
1868
|
files: [],
|
|
1894
1869
|
},
|
|
1895
|
-
isError: false,
|
|
1896
1870
|
},
|
|
1897
1871
|
{
|
|
1898
1872
|
role: "assistant",
|
|
@@ -1939,7 +1913,6 @@ export const longSession = {
|
|
|
1939
1913
|
details: {
|
|
1940
1914
|
files: [],
|
|
1941
1915
|
},
|
|
1942
|
-
isError: false,
|
|
1943
1916
|
},
|
|
1944
1917
|
{
|
|
1945
1918
|
role: "assistant",
|
|
@@ -1986,7 +1959,6 @@ export const longSession = {
|
|
|
1986
1959
|
details: {
|
|
1987
1960
|
files: [],
|
|
1988
1961
|
},
|
|
1989
|
-
isError: false,
|
|
1990
1962
|
},
|
|
1991
1963
|
{
|
|
1992
1964
|
role: "assistant",
|
|
@@ -2033,7 +2005,6 @@ export const longSession = {
|
|
|
2033
2005
|
details: {
|
|
2034
2006
|
files: [],
|
|
2035
2007
|
},
|
|
2036
|
-
isError: false,
|
|
2037
2008
|
},
|
|
2038
2009
|
{
|
|
2039
2010
|
role: "assistant",
|
|
@@ -2080,7 +2051,6 @@ export const longSession = {
|
|
|
2080
2051
|
toolCallId: "toolu_01J26GUEiATmMTYeMDJnmAFN",
|
|
2081
2052
|
toolName: "artifacts",
|
|
2082
2053
|
output: "Created file liveblog_ukraine.md",
|
|
2083
|
-
isError: false,
|
|
2084
2054
|
},
|
|
2085
2055
|
{
|
|
2086
2056
|
role: "assistant",
|
|
@@ -2160,7 +2130,6 @@ export const longSession = {
|
|
|
2160
2130
|
toolCallId: "toolu_012E1DjRwg1wgZhD38mBNpSv",
|
|
2161
2131
|
toolName: "artifacts",
|
|
2162
2132
|
output: "Created file minimal.html",
|
|
2163
|
-
isError: false,
|
|
2164
2133
|
},
|
|
2165
2134
|
{
|
|
2166
2135
|
role: "assistant",
|
|
@@ -2241,7 +2210,6 @@ export const longSession = {
|
|
|
2241
2210
|
toolName: "artifacts",
|
|
2242
2211
|
output:
|
|
2243
2212
|
"Updated file index.html\n\nExecution timed out. Partial logs:\n[log] Page loaded successfully!\n[log] Welcome to the simple HTML page",
|
|
2244
|
-
isError: false,
|
|
2245
2213
|
},
|
|
2246
2214
|
{
|
|
2247
2215
|
role: "assistant",
|
|
@@ -2323,7 +2291,6 @@ export const longSession = {
|
|
|
2323
2291
|
toolName: "artifacts",
|
|
2324
2292
|
output:
|
|
2325
2293
|
"Updated file index.html\n\nExecution timed out. Partial logs:\n[log] Page loaded successfully!\n[log] Welcome to the simple HTML page\n[log] Third console log added!",
|
|
2326
|
-
isError: false,
|
|
2327
2294
|
},
|
|
2328
2295
|
{
|
|
2329
2296
|
role: "assistant",
|