@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 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.13.1337",
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.13.1337",
23
- "@oh-my-pi/pi-ai": "3.13.1337",
24
- "@oh-my-pi/pi-tui": "3.13.1337",
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 uint8Array = new Uint8Array(arrayBuffer);
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
- return {
158
- id,
159
- type: "image",
160
- fileName: detectedFileName,
161
- mimeType,
162
- size,
163
- content: base64Content,
164
- preview: base64Content, // For images, preview is the same as content
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",