@oh-my-pi/pi-web-ui 1.337.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.
Files changed (86) hide show
  1. package/CHANGELOG.md +96 -0
  2. package/README.md +609 -0
  3. package/example/README.md +61 -0
  4. package/example/index.html +13 -0
  5. package/example/package.json +24 -0
  6. package/example/src/app.css +1 -0
  7. package/example/src/custom-messages.ts +99 -0
  8. package/example/src/main.ts +420 -0
  9. package/example/tsconfig.json +23 -0
  10. package/example/vite.config.ts +6 -0
  11. package/package.json +57 -0
  12. package/scripts/count-prompt-tokens.ts +88 -0
  13. package/src/ChatPanel.ts +218 -0
  14. package/src/app.css +68 -0
  15. package/src/components/AgentInterface.ts +390 -0
  16. package/src/components/AttachmentTile.ts +107 -0
  17. package/src/components/ConsoleBlock.ts +74 -0
  18. package/src/components/CustomProviderCard.ts +96 -0
  19. package/src/components/ExpandableSection.ts +46 -0
  20. package/src/components/Input.ts +113 -0
  21. package/src/components/MessageEditor.ts +404 -0
  22. package/src/components/MessageList.ts +97 -0
  23. package/src/components/Messages.ts +384 -0
  24. package/src/components/ProviderKeyInput.ts +152 -0
  25. package/src/components/SandboxedIframe.ts +626 -0
  26. package/src/components/StreamingMessageContainer.ts +107 -0
  27. package/src/components/ThinkingBlock.ts +45 -0
  28. package/src/components/message-renderer-registry.ts +28 -0
  29. package/src/components/sandbox/ArtifactsRuntimeProvider.ts +219 -0
  30. package/src/components/sandbox/AttachmentsRuntimeProvider.ts +66 -0
  31. package/src/components/sandbox/ConsoleRuntimeProvider.ts +186 -0
  32. package/src/components/sandbox/FileDownloadRuntimeProvider.ts +110 -0
  33. package/src/components/sandbox/RuntimeMessageBridge.ts +82 -0
  34. package/src/components/sandbox/RuntimeMessageRouter.ts +216 -0
  35. package/src/components/sandbox/SandboxRuntimeProvider.ts +52 -0
  36. package/src/dialogs/ApiKeyPromptDialog.ts +75 -0
  37. package/src/dialogs/AttachmentOverlay.ts +640 -0
  38. package/src/dialogs/CustomProviderDialog.ts +274 -0
  39. package/src/dialogs/ModelSelector.ts +314 -0
  40. package/src/dialogs/PersistentStorageDialog.ts +146 -0
  41. package/src/dialogs/ProvidersModelsTab.ts +212 -0
  42. package/src/dialogs/SessionListDialog.ts +157 -0
  43. package/src/dialogs/SettingsDialog.ts +216 -0
  44. package/src/index.ts +115 -0
  45. package/src/prompts/prompts.ts +282 -0
  46. package/src/storage/app-storage.ts +60 -0
  47. package/src/storage/backends/indexeddb-storage-backend.ts +193 -0
  48. package/src/storage/store.ts +33 -0
  49. package/src/storage/stores/custom-providers-store.ts +62 -0
  50. package/src/storage/stores/provider-keys-store.ts +33 -0
  51. package/src/storage/stores/sessions-store.ts +136 -0
  52. package/src/storage/stores/settings-store.ts +34 -0
  53. package/src/storage/types.ts +206 -0
  54. package/src/tools/artifacts/ArtifactElement.ts +14 -0
  55. package/src/tools/artifacts/ArtifactPill.ts +26 -0
  56. package/src/tools/artifacts/Console.ts +102 -0
  57. package/src/tools/artifacts/DocxArtifact.ts +213 -0
  58. package/src/tools/artifacts/ExcelArtifact.ts +231 -0
  59. package/src/tools/artifacts/GenericArtifact.ts +118 -0
  60. package/src/tools/artifacts/HtmlArtifact.ts +203 -0
  61. package/src/tools/artifacts/ImageArtifact.ts +116 -0
  62. package/src/tools/artifacts/MarkdownArtifact.ts +83 -0
  63. package/src/tools/artifacts/PdfArtifact.ts +201 -0
  64. package/src/tools/artifacts/SvgArtifact.ts +82 -0
  65. package/src/tools/artifacts/TextArtifact.ts +148 -0
  66. package/src/tools/artifacts/artifacts-tool-renderer.ts +371 -0
  67. package/src/tools/artifacts/artifacts.ts +713 -0
  68. package/src/tools/artifacts/index.ts +7 -0
  69. package/src/tools/extract-document.ts +271 -0
  70. package/src/tools/index.ts +46 -0
  71. package/src/tools/javascript-repl.ts +316 -0
  72. package/src/tools/renderer-registry.ts +127 -0
  73. package/src/tools/renderers/BashRenderer.ts +52 -0
  74. package/src/tools/renderers/CalculateRenderer.ts +58 -0
  75. package/src/tools/renderers/DefaultRenderer.ts +95 -0
  76. package/src/tools/renderers/GetCurrentTimeRenderer.ts +92 -0
  77. package/src/tools/types.ts +15 -0
  78. package/src/utils/attachment-utils.ts +472 -0
  79. package/src/utils/auth-token.ts +22 -0
  80. package/src/utils/format.ts +42 -0
  81. package/src/utils/i18n.ts +653 -0
  82. package/src/utils/model-discovery.ts +277 -0
  83. package/src/utils/proxy-utils.ts +134 -0
  84. package/src/utils/test-sessions.ts +2357 -0
  85. package/tsconfig.build.json +20 -0
  86. package/tsconfig.json +7 -0
@@ -0,0 +1,640 @@
1
+ import "@mariozechner/mini-lit/dist/ModeToggle.js";
2
+ import { icon } from "@mariozechner/mini-lit";
3
+ import { Button } from "@mariozechner/mini-lit/dist/Button.js";
4
+ import { renderAsync } from "docx-preview";
5
+ import { html, LitElement } from "lit";
6
+ import { state } from "lit/decorators.js";
7
+ import { Download, X } from "lucide";
8
+ import * as pdfjsLib from "pdfjs-dist";
9
+ import * as XLSX from "xlsx";
10
+ import type { Attachment } from "../utils/attachment-utils.js";
11
+ import { i18n } from "../utils/i18n.js";
12
+
13
+ type FileType = "image" | "pdf" | "docx" | "pptx" | "excel" | "text";
14
+
15
+ export class AttachmentOverlay extends LitElement {
16
+ @state() private attachment?: Attachment;
17
+ @state() private showExtractedText = false;
18
+ @state() private error: string | null = null;
19
+
20
+ // Track current loading task to cancel if needed
21
+ private currentLoadingTask: any = null;
22
+ private onCloseCallback?: () => void;
23
+ private boundHandleKeyDown?: (e: KeyboardEvent) => void;
24
+
25
+ protected override createRenderRoot(): HTMLElement | DocumentFragment {
26
+ return this;
27
+ }
28
+
29
+ static open(attachment: Attachment, onClose?: () => void) {
30
+ const overlay = new AttachmentOverlay();
31
+ overlay.attachment = attachment;
32
+ overlay.onCloseCallback = onClose;
33
+ document.body.appendChild(overlay);
34
+ overlay.setupEventListeners();
35
+ }
36
+
37
+ private setupEventListeners() {
38
+ this.boundHandleKeyDown = (e: KeyboardEvent) => {
39
+ if (e.key === "Escape") {
40
+ this.close();
41
+ }
42
+ };
43
+ window.addEventListener("keydown", this.boundHandleKeyDown);
44
+ }
45
+
46
+ private close() {
47
+ this.cleanup();
48
+ if (this.boundHandleKeyDown) {
49
+ window.removeEventListener("keydown", this.boundHandleKeyDown);
50
+ }
51
+ this.onCloseCallback?.();
52
+ this.remove();
53
+ }
54
+
55
+ private getFileType(): FileType {
56
+ if (!this.attachment) return "text";
57
+
58
+ if (this.attachment.type === "image") return "image";
59
+ if (this.attachment.mimeType === "application/pdf") return "pdf";
60
+ if (this.attachment.mimeType?.includes("wordprocessingml")) return "docx";
61
+ if (
62
+ this.attachment.mimeType?.includes("presentationml") ||
63
+ this.attachment.fileName.toLowerCase().endsWith(".pptx")
64
+ )
65
+ return "pptx";
66
+ if (
67
+ this.attachment.mimeType?.includes("spreadsheetml") ||
68
+ this.attachment.mimeType?.includes("ms-excel") ||
69
+ this.attachment.fileName.toLowerCase().endsWith(".xlsx") ||
70
+ this.attachment.fileName.toLowerCase().endsWith(".xls")
71
+ )
72
+ return "excel";
73
+
74
+ return "text";
75
+ }
76
+
77
+ private getFileTypeLabel(): string {
78
+ const type = this.getFileType();
79
+ switch (type) {
80
+ case "pdf":
81
+ return i18n("PDF");
82
+ case "docx":
83
+ return i18n("Document");
84
+ case "pptx":
85
+ return i18n("Presentation");
86
+ case "excel":
87
+ return i18n("Spreadsheet");
88
+ default:
89
+ return "";
90
+ }
91
+ }
92
+
93
+ private handleBackdropClick = () => {
94
+ this.close();
95
+ };
96
+
97
+ private handleDownload = () => {
98
+ if (!this.attachment) return;
99
+
100
+ // Create a blob from the base64 content
101
+ const byteCharacters = atob(this.attachment.content);
102
+ const byteNumbers = new Array(byteCharacters.length);
103
+ for (let i = 0; i < byteCharacters.length; i++) {
104
+ byteNumbers[i] = byteCharacters.charCodeAt(i);
105
+ }
106
+ const byteArray = new Uint8Array(byteNumbers);
107
+ const blob = new Blob([byteArray], { type: this.attachment.mimeType });
108
+
109
+ // Create download link
110
+ const url = URL.createObjectURL(blob);
111
+ const a = document.createElement("a");
112
+ a.href = url;
113
+ a.download = this.attachment.fileName;
114
+ document.body.appendChild(a);
115
+ a.click();
116
+ document.body.removeChild(a);
117
+ URL.revokeObjectURL(url);
118
+ };
119
+
120
+ private cleanup() {
121
+ this.showExtractedText = false;
122
+ this.error = null;
123
+ // Cancel any loading PDF task when closing
124
+ if (this.currentLoadingTask) {
125
+ this.currentLoadingTask.destroy();
126
+ this.currentLoadingTask = null;
127
+ }
128
+ }
129
+
130
+ override render() {
131
+ if (!this.attachment) return html``;
132
+
133
+ return html`
134
+ <!-- Full screen overlay -->
135
+ <div class="fixed inset-0 bg-black/90 z-50 flex flex-col" @click=${this.handleBackdropClick}>
136
+ <!-- Compact header bar -->
137
+ <div class="bg-background/95 backdrop-blur border-b border-border" @click=${(e: Event) => e.stopPropagation()}>
138
+ <div class="px-4 py-2 flex items-center justify-between">
139
+ <div class="flex items-center gap-3 min-w-0">
140
+ <span class="text-sm font-medium text-foreground truncate">${this.attachment.fileName}</span>
141
+ </div>
142
+ <div class="flex items-center gap-2">
143
+ ${this.renderToggle()}
144
+ ${Button({
145
+ variant: "ghost",
146
+ size: "icon",
147
+ onClick: this.handleDownload,
148
+ children: icon(Download, "sm"),
149
+ className: "h-8 w-8",
150
+ })}
151
+ ${Button({
152
+ variant: "ghost",
153
+ size: "icon",
154
+ onClick: () => this.close(),
155
+ children: icon(X, "sm"),
156
+ className: "h-8 w-8",
157
+ })}
158
+ </div>
159
+ </div>
160
+ </div>
161
+
162
+ <!-- Content container -->
163
+ <div class="flex-1 flex items-center justify-center overflow-auto" @click=${(e: Event) => e.stopPropagation()}>
164
+ ${this.renderContent()}
165
+ </div>
166
+ </div>
167
+ `;
168
+ }
169
+
170
+ private renderToggle() {
171
+ if (!this.attachment) return html``;
172
+
173
+ const fileType = this.getFileType();
174
+ const hasExtractedText = !!this.attachment.extractedText;
175
+ const showToggle = fileType !== "image" && fileType !== "text" && fileType !== "pptx" && hasExtractedText;
176
+
177
+ if (!showToggle) return html``;
178
+
179
+ const fileTypeLabel = this.getFileTypeLabel();
180
+
181
+ return html`
182
+ <mode-toggle
183
+ .modes=${[fileTypeLabel, i18n("Text")]}
184
+ .selectedIndex=${this.showExtractedText ? 1 : 0}
185
+ @mode-change=${(e: CustomEvent<{ index: number; mode: string }>) => {
186
+ e.stopPropagation();
187
+ this.showExtractedText = e.detail.index === 1;
188
+ this.error = null;
189
+ }}
190
+ ></mode-toggle>
191
+ `;
192
+ }
193
+
194
+ private renderContent() {
195
+ if (!this.attachment) return html``;
196
+
197
+ // Error state
198
+ if (this.error) {
199
+ return html`
200
+ <div class="bg-destructive/10 border border-destructive text-destructive p-4 rounded-lg max-w-2xl">
201
+ <div class="font-medium mb-1">${i18n("Error loading file")}</div>
202
+ <div class="text-sm opacity-90">${this.error}</div>
203
+ </div>
204
+ `;
205
+ }
206
+
207
+ // Content based on file type
208
+ return this.renderFileContent();
209
+ }
210
+
211
+ private renderFileContent() {
212
+ if (!this.attachment) return html``;
213
+
214
+ const fileType = this.getFileType();
215
+
216
+ // Show extracted text if toggled
217
+ if (this.showExtractedText && fileType !== "image") {
218
+ return html`
219
+ <div class="bg-card border border-border text-foreground p-6 w-full h-full max-w-4xl overflow-auto">
220
+ <pre class="whitespace-pre-wrap font-mono text-xs leading-relaxed">
221
+ ${this.attachment.extractedText || i18n("No text content available")}</pre
222
+ >
223
+ </div>
224
+ `;
225
+ }
226
+
227
+ // Render based on file type
228
+ switch (fileType) {
229
+ case "image": {
230
+ const imageUrl = `data:${this.attachment.mimeType};base64,${this.attachment.content}`;
231
+ return html`
232
+ <img
233
+ src="${imageUrl}"
234
+ class="max-w-full max-h-full object-contain rounded-lg shadow-lg"
235
+ alt="${this.attachment.fileName}"
236
+ />
237
+ `;
238
+ }
239
+
240
+ case "pdf":
241
+ return html`
242
+ <div
243
+ id="pdf-container"
244
+ class="bg-card text-foreground overflow-auto shadow-lg border border-border w-full h-full max-w-[1000px]"
245
+ ></div>
246
+ `;
247
+
248
+ case "docx":
249
+ return html`
250
+ <div
251
+ id="docx-container"
252
+ class="bg-card text-foreground overflow-auto shadow-lg border border-border w-full h-full max-w-[1000px]"
253
+ ></div>
254
+ `;
255
+
256
+ case "excel":
257
+ return html` <div id="excel-container" class="bg-card text-foreground overflow-auto w-full h-full"></div> `;
258
+
259
+ case "pptx":
260
+ return html`
261
+ <div
262
+ id="pptx-container"
263
+ class="bg-card text-foreground overflow-auto shadow-lg border border-border w-full h-full max-w-[1000px]"
264
+ ></div>
265
+ `;
266
+
267
+ default:
268
+ return html`
269
+ <div class="bg-card border border-border text-foreground p-6 w-full h-full max-w-4xl overflow-auto">
270
+ <pre class="whitespace-pre-wrap font-mono text-sm">
271
+ ${this.attachment.extractedText || i18n("No content available")}</pre
272
+ >
273
+ </div>
274
+ `;
275
+ }
276
+ }
277
+
278
+ override async updated(changedProperties: Map<string, any>) {
279
+ super.updated(changedProperties);
280
+
281
+ // Only process if we need to render the actual file (not extracted text)
282
+ if (
283
+ (changedProperties.has("attachment") || changedProperties.has("showExtractedText")) &&
284
+ this.attachment &&
285
+ !this.showExtractedText &&
286
+ !this.error
287
+ ) {
288
+ const fileType = this.getFileType();
289
+
290
+ switch (fileType) {
291
+ case "pdf":
292
+ await this.renderPdf();
293
+ break;
294
+ case "docx":
295
+ await this.renderDocx();
296
+ break;
297
+ case "excel":
298
+ await this.renderExcel();
299
+ break;
300
+ case "pptx":
301
+ await this.renderExtractedText();
302
+ break;
303
+ }
304
+ }
305
+ }
306
+
307
+ private async renderPdf() {
308
+ const container = this.querySelector("#pdf-container");
309
+ if (!container || !this.attachment) return;
310
+
311
+ let pdf: any = null;
312
+
313
+ try {
314
+ // Convert base64 to ArrayBuffer
315
+ const arrayBuffer = this.base64ToArrayBuffer(this.attachment.content);
316
+
317
+ // Cancel any existing loading task
318
+ if (this.currentLoadingTask) {
319
+ this.currentLoadingTask.destroy();
320
+ }
321
+
322
+ // Load the PDF
323
+ this.currentLoadingTask = pdfjsLib.getDocument({ data: arrayBuffer });
324
+ pdf = await this.currentLoadingTask.promise;
325
+ this.currentLoadingTask = null;
326
+
327
+ // Clear container and add wrapper
328
+ container.innerHTML = "";
329
+ const wrapper = document.createElement("div");
330
+ wrapper.className = "";
331
+ container.appendChild(wrapper);
332
+
333
+ // Render all pages
334
+ for (let pageNum = 1; pageNum <= pdf.numPages; pageNum++) {
335
+ const page = await pdf.getPage(pageNum);
336
+
337
+ // Create a container for each page
338
+ const pageContainer = document.createElement("div");
339
+ pageContainer.className = "mb-4 last:mb-0";
340
+
341
+ // Create canvas for this page
342
+ const canvas = document.createElement("canvas");
343
+ const context = canvas.getContext("2d");
344
+
345
+ // Set scale for reasonable resolution
346
+ const viewport = page.getViewport({ scale: 1.5 });
347
+ canvas.height = viewport.height;
348
+ canvas.width = viewport.width;
349
+
350
+ // Style the canvas
351
+ canvas.className = "w-full max-w-full h-auto block mx-auto bg-white rounded shadow-sm border border-border";
352
+
353
+ // Fill white background for proper PDF rendering
354
+ if (context) {
355
+ context.fillStyle = "white";
356
+ context.fillRect(0, 0, canvas.width, canvas.height);
357
+ }
358
+
359
+ // Render page
360
+ await page.render({
361
+ canvasContext: context!,
362
+ viewport: viewport,
363
+ canvas: canvas,
364
+ }).promise;
365
+
366
+ pageContainer.appendChild(canvas);
367
+
368
+ // Add page separator for multi-page documents
369
+ if (pageNum < pdf.numPages) {
370
+ const separator = document.createElement("div");
371
+ separator.className = "h-px bg-border my-4";
372
+ pageContainer.appendChild(separator);
373
+ }
374
+
375
+ wrapper.appendChild(pageContainer);
376
+ }
377
+ } catch (error: any) {
378
+ console.error("Error rendering PDF:", error);
379
+ this.error = error?.message || i18n("Failed to load PDF");
380
+ } finally {
381
+ if (pdf) {
382
+ pdf.destroy();
383
+ }
384
+ }
385
+ }
386
+
387
+ private async renderDocx() {
388
+ const container = this.querySelector("#docx-container");
389
+ if (!container || !this.attachment) return;
390
+
391
+ try {
392
+ // Convert base64 to ArrayBuffer
393
+ const arrayBuffer = this.base64ToArrayBuffer(this.attachment.content);
394
+
395
+ // Clear container first
396
+ container.innerHTML = "";
397
+
398
+ // Create a wrapper div for the document
399
+ const wrapper = document.createElement("div");
400
+ wrapper.className = "docx-wrapper-custom";
401
+ container.appendChild(wrapper);
402
+
403
+ // Render the DOCX file into the wrapper
404
+ await renderAsync(arrayBuffer, wrapper as HTMLElement, undefined, {
405
+ className: "docx",
406
+ inWrapper: true,
407
+ ignoreWidth: true, // Let it be responsive
408
+ ignoreHeight: false,
409
+ ignoreFonts: false,
410
+ breakPages: true,
411
+ ignoreLastRenderedPageBreak: true,
412
+ experimental: false,
413
+ trimXmlDeclaration: true,
414
+ useBase64URL: false,
415
+ renderHeaders: true,
416
+ renderFooters: true,
417
+ renderFootnotes: true,
418
+ renderEndnotes: true,
419
+ });
420
+
421
+ // Apply custom styles to match theme and fix sizing
422
+ const style = document.createElement("style");
423
+ style.textContent = `
424
+ #docx-container {
425
+ padding: 0;
426
+ }
427
+
428
+ #docx-container .docx-wrapper-custom {
429
+ max-width: 100%;
430
+ overflow-x: auto;
431
+ }
432
+
433
+ #docx-container .docx-wrapper {
434
+ max-width: 100% !important;
435
+ margin: 0 !important;
436
+ background: transparent !important;
437
+ padding: 0em !important;
438
+ }
439
+
440
+ #docx-container .docx-wrapper > section.docx {
441
+ box-shadow: none !important;
442
+ border: none !important;
443
+ border-radius: 0 !important;
444
+ margin: 0 !important;
445
+ padding: 2em !important;
446
+ background: white !important;
447
+ color: black !important;
448
+ max-width: 100% !important;
449
+ width: 100% !important;
450
+ min-width: 0 !important;
451
+ overflow-x: auto !important;
452
+ }
453
+
454
+ /* Fix tables and wide content */
455
+ #docx-container table {
456
+ max-width: 100% !important;
457
+ width: auto !important;
458
+ overflow-x: auto !important;
459
+ display: block !important;
460
+ }
461
+
462
+ #docx-container img {
463
+ max-width: 100% !important;
464
+ height: auto !important;
465
+ }
466
+
467
+ /* Fix paragraphs and text */
468
+ #docx-container p,
469
+ #docx-container span,
470
+ #docx-container div {
471
+ max-width: 100% !important;
472
+ word-wrap: break-word !important;
473
+ overflow-wrap: break-word !important;
474
+ }
475
+
476
+ /* Hide page breaks in web view */
477
+ #docx-container .docx-page-break {
478
+ display: none !important;
479
+ }
480
+ `;
481
+ container.appendChild(style);
482
+ } catch (error: any) {
483
+ console.error("Error rendering DOCX:", error);
484
+ this.error = error?.message || i18n("Failed to load document");
485
+ }
486
+ }
487
+
488
+ private async renderExcel() {
489
+ const container = this.querySelector("#excel-container");
490
+ if (!container || !this.attachment) return;
491
+
492
+ try {
493
+ // Convert base64 to ArrayBuffer
494
+ const arrayBuffer = this.base64ToArrayBuffer(this.attachment.content);
495
+
496
+ // Read the workbook
497
+ const workbook = XLSX.read(arrayBuffer, { type: "array" });
498
+
499
+ // Clear container
500
+ container.innerHTML = "";
501
+ const wrapper = document.createElement("div");
502
+ wrapper.className = "overflow-auto h-full flex flex-col";
503
+ container.appendChild(wrapper);
504
+
505
+ // Create tabs for multiple sheets
506
+ if (workbook.SheetNames.length > 1) {
507
+ const tabContainer = document.createElement("div");
508
+ tabContainer.className = "flex gap-2 mb-4 border-b border-border sticky top-0 bg-card z-10";
509
+
510
+ const sheetContents: HTMLElement[] = [];
511
+
512
+ workbook.SheetNames.forEach((sheetName, index) => {
513
+ // Create tab button
514
+ const tab = document.createElement("button");
515
+ tab.textContent = sheetName;
516
+ tab.className =
517
+ index === 0
518
+ ? "px-4 py-2 text-sm font-medium border-b-2 border-primary text-primary"
519
+ : "px-4 py-2 text-sm font-medium text-muted-foreground hover:text-foreground hover:border-b-2 hover:border-border transition-colors";
520
+
521
+ // Create sheet content
522
+ const sheetDiv = document.createElement("div");
523
+ sheetDiv.style.display = index === 0 ? "flex" : "none";
524
+ sheetDiv.className = "flex-1 overflow-auto";
525
+ sheetDiv.appendChild(this.renderExcelSheet(workbook.Sheets[sheetName], sheetName));
526
+ sheetContents.push(sheetDiv);
527
+
528
+ // Tab click handler
529
+ tab.onclick = () => {
530
+ // Update tab styles
531
+ tabContainer.querySelectorAll("button").forEach((btn, btnIndex) => {
532
+ if (btnIndex === index) {
533
+ btn.className = "px-4 py-2 text-sm font-medium border-b-2 border-primary text-primary";
534
+ } else {
535
+ btn.className =
536
+ "px-4 py-2 text-sm font-medium text-muted-foreground hover:text-foreground hover:border-b-2 hover:border-border transition-colors";
537
+ }
538
+ });
539
+ // Show/hide sheets
540
+ sheetContents.forEach((content, contentIndex) => {
541
+ content.style.display = contentIndex === index ? "flex" : "none";
542
+ });
543
+ };
544
+
545
+ tabContainer.appendChild(tab);
546
+ });
547
+
548
+ wrapper.appendChild(tabContainer);
549
+ sheetContents.forEach((content) => {
550
+ wrapper.appendChild(content);
551
+ });
552
+ } else {
553
+ // Single sheet
554
+ const sheetName = workbook.SheetNames[0];
555
+ wrapper.appendChild(this.renderExcelSheet(workbook.Sheets[sheetName], sheetName));
556
+ }
557
+ } catch (error: any) {
558
+ console.error("Error rendering Excel:", error);
559
+ this.error = error?.message || i18n("Failed to load spreadsheet");
560
+ }
561
+ }
562
+
563
+ private renderExcelSheet(worksheet: any, sheetName: string): HTMLElement {
564
+ const sheetDiv = document.createElement("div");
565
+
566
+ // Generate HTML table
567
+ const htmlTable = XLSX.utils.sheet_to_html(worksheet, { id: `sheet-${sheetName}` });
568
+ const tempDiv = document.createElement("div");
569
+ tempDiv.innerHTML = htmlTable;
570
+
571
+ // Find and style the table
572
+ const table = tempDiv.querySelector("table");
573
+ if (table) {
574
+ table.className = "w-full border-collapse text-foreground";
575
+
576
+ // Style all cells
577
+ table.querySelectorAll("td, th").forEach((cell) => {
578
+ const cellEl = cell as HTMLElement;
579
+ cellEl.className = "border border-border px-3 py-2 text-sm text-left";
580
+ });
581
+
582
+ // Style header row
583
+ const headerCells = table.querySelectorAll("thead th, tr:first-child td");
584
+ if (headerCells.length > 0) {
585
+ headerCells.forEach((th) => {
586
+ const thEl = th as HTMLElement;
587
+ thEl.className =
588
+ "border border-border px-3 py-2 text-sm font-semibold bg-muted text-foreground sticky top-0";
589
+ });
590
+ }
591
+
592
+ // Alternate row colors
593
+ table.querySelectorAll("tbody tr:nth-child(even)").forEach((row) => {
594
+ const rowEl = row as HTMLElement;
595
+ rowEl.className = "bg-muted/30";
596
+ });
597
+
598
+ sheetDiv.appendChild(table);
599
+ }
600
+
601
+ return sheetDiv;
602
+ }
603
+
604
+ private base64ToArrayBuffer(base64: string): ArrayBuffer {
605
+ const binaryString = atob(base64);
606
+ const bytes = new Uint8Array(binaryString.length);
607
+ for (let i = 0; i < binaryString.length; i++) {
608
+ bytes[i] = binaryString.charCodeAt(i);
609
+ }
610
+ return bytes.buffer;
611
+ }
612
+
613
+ private async renderExtractedText() {
614
+ const container = this.querySelector("#pptx-container");
615
+ if (!container || !this.attachment) return;
616
+
617
+ try {
618
+ // Display the extracted text content
619
+ container.innerHTML = "";
620
+ const wrapper = document.createElement("div");
621
+ wrapper.className = "p-6 overflow-auto";
622
+
623
+ // Create a pre element to preserve formatting
624
+ const pre = document.createElement("pre");
625
+ pre.className = "whitespace-pre-wrap text-sm text-foreground font-mono";
626
+ pre.textContent = this.attachment.extractedText || i18n("No text content available");
627
+
628
+ wrapper.appendChild(pre);
629
+ container.appendChild(wrapper);
630
+ } catch (error: any) {
631
+ console.error("Error rendering extracted text:", error);
632
+ this.error = error?.message || i18n("Failed to display text content");
633
+ }
634
+ }
635
+ }
636
+
637
+ // Register the custom element only once
638
+ if (!customElements.get("attachment-overlay")) {
639
+ customElements.define("attachment-overlay", AttachmentOverlay);
640
+ }