@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,203 @@
1
+ import hljs from "highlight.js";
2
+ import { html } from "lit";
3
+ import { customElement, property, state } from "lit/decorators.js";
4
+ import { createRef, type Ref, ref } from "lit/directives/ref.js";
5
+ import { unsafeHTML } from "lit/directives/unsafe-html.js";
6
+ import { RefreshCw } from "lucide";
7
+ import type { SandboxIframe } from "../../components/SandboxedIframe.js";
8
+ import { type MessageConsumer, RUNTIME_MESSAGE_ROUTER } from "../../components/sandbox/RuntimeMessageRouter.js";
9
+ import type { SandboxRuntimeProvider } from "../../components/sandbox/SandboxRuntimeProvider.js";
10
+ import { i18n } from "../../utils/i18n.js";
11
+ import "../../components/SandboxedIframe.js";
12
+ import { ArtifactElement } from "./ArtifactElement.js";
13
+ import type { Console } from "./Console.js";
14
+ import "./Console.js";
15
+ import { icon } from "@mariozechner/mini-lit";
16
+ import { Button } from "@mariozechner/mini-lit/dist/Button.js";
17
+ import { CopyButton } from "@mariozechner/mini-lit/dist/CopyButton.js";
18
+ import { DownloadButton } from "@mariozechner/mini-lit/dist/DownloadButton.js";
19
+ import { PreviewCodeToggle } from "@mariozechner/mini-lit/dist/PreviewCodeToggle.js";
20
+
21
+ @customElement("html-artifact")
22
+ export class HtmlArtifact extends ArtifactElement {
23
+ @property() override filename = "";
24
+ @property({ attribute: false }) runtimeProviders: SandboxRuntimeProvider[] = [];
25
+ @property({ attribute: false }) sandboxUrlProvider?: () => string;
26
+
27
+ private _content = "";
28
+ private logs: Array<{ type: "log" | "error"; text: string }> = [];
29
+
30
+ // Refs for DOM elements
31
+ public sandboxIframeRef: Ref<SandboxIframe> = createRef();
32
+ private consoleRef: Ref<Console> = createRef();
33
+
34
+ @state() private viewMode: "preview" | "code" = "preview";
35
+
36
+ private setViewMode(mode: "preview" | "code") {
37
+ this.viewMode = mode;
38
+ }
39
+
40
+ public getHeaderButtons() {
41
+ const toggle = new PreviewCodeToggle();
42
+ toggle.mode = this.viewMode;
43
+ toggle.addEventListener("mode-change", (e: Event) => {
44
+ this.setViewMode((e as CustomEvent).detail);
45
+ });
46
+
47
+ const copyButton = new CopyButton();
48
+ copyButton.text = this._content;
49
+ copyButton.title = i18n("Copy HTML");
50
+ copyButton.showText = false;
51
+
52
+ // Generate standalone HTML with all runtime code injected for download
53
+ const sandbox = this.sandboxIframeRef.value;
54
+ const sandboxId = `artifact-${this.filename}`;
55
+ const downloadContent =
56
+ sandbox?.prepareHtmlDocument(sandboxId, this._content, this.runtimeProviders || [], {
57
+ isHtmlArtifact: true,
58
+ isStandalone: true, // Skip runtime bridge and navigation interceptor for standalone downloads
59
+ }) || this._content;
60
+
61
+ return html`
62
+ <div class="flex items-center gap-2">
63
+ ${toggle}
64
+ ${Button({
65
+ variant: "ghost",
66
+ size: "sm",
67
+ onClick: () => {
68
+ this.logs = [];
69
+ this.executeContent(this._content);
70
+ },
71
+ title: i18n("Reload HTML"),
72
+ children: icon(RefreshCw, "sm"),
73
+ })}
74
+ ${copyButton}
75
+ ${DownloadButton({
76
+ content: downloadContent,
77
+ filename: this.filename,
78
+ mimeType: "text/html",
79
+ title: i18n("Download HTML"),
80
+ })}
81
+ </div>
82
+ `;
83
+ }
84
+
85
+ override set content(value: string) {
86
+ const oldValue = this._content;
87
+ this._content = value;
88
+ if (oldValue !== value) {
89
+ // Reset logs when content changes
90
+ this.logs = [];
91
+ this.requestUpdate();
92
+ // Execute content in sandbox if it exists
93
+ if (this.sandboxIframeRef.value && value) {
94
+ this.executeContent(value);
95
+ }
96
+ }
97
+ }
98
+
99
+ public executeContent(html: string) {
100
+ const sandbox = this.sandboxIframeRef.value;
101
+ if (!sandbox) return;
102
+
103
+ // Configure sandbox URL provider if provided (for browser extensions)
104
+ if (this.sandboxUrlProvider) {
105
+ sandbox.sandboxUrlProvider = this.sandboxUrlProvider;
106
+ }
107
+
108
+ const sandboxId = `artifact-${this.filename}`;
109
+
110
+ // Create consumer for console messages
111
+ const consumer: MessageConsumer = {
112
+ handleMessage: async (message: any): Promise<void> => {
113
+ if (message.type === "console") {
114
+ // Create new array reference for Lit reactivity
115
+ this.logs = [
116
+ ...this.logs,
117
+ {
118
+ type: message.method === "error" ? "error" : "log",
119
+ text: message.text,
120
+ },
121
+ ];
122
+ this.requestUpdate(); // Re-render to show console
123
+ }
124
+ },
125
+ };
126
+
127
+ // Inject window.complete() call at the end of the HTML to signal when page is loaded
128
+ // HTML artifacts don't time out - they call complete() when ready
129
+ let modifiedHtml = html;
130
+ if (modifiedHtml.includes("</html>")) {
131
+ modifiedHtml = modifiedHtml.replace(
132
+ "</html>",
133
+ "<script>if (window.complete) window.complete();</script></html>",
134
+ );
135
+ } else {
136
+ // If no closing </html> tag, append the script
137
+ modifiedHtml += "<script>if (window.complete) window.complete();</script>";
138
+ }
139
+
140
+ // Load content - this handles sandbox registration, consumer registration, and iframe creation
141
+ sandbox.loadContent(sandboxId, modifiedHtml, this.runtimeProviders, [consumer]);
142
+ }
143
+
144
+ override get content(): string {
145
+ return this._content;
146
+ }
147
+
148
+ override disconnectedCallback() {
149
+ super.disconnectedCallback();
150
+ // Unregister sandbox when element is removed from DOM
151
+ const sandboxId = `artifact-${this.filename}`;
152
+ RUNTIME_MESSAGE_ROUTER.unregisterSandbox(sandboxId);
153
+ }
154
+
155
+ override firstUpdated() {
156
+ // Execute initial content
157
+ if (this._content && this.sandboxIframeRef.value) {
158
+ this.executeContent(this._content);
159
+ }
160
+ }
161
+
162
+ override updated(changedProperties: Map<string | number | symbol, unknown>) {
163
+ super.updated(changedProperties);
164
+ // If we have content but haven't executed yet (e.g., during reconstruction),
165
+ // execute when the iframe ref becomes available
166
+ if (this._content && this.sandboxIframeRef.value && this.logs.length === 0) {
167
+ this.executeContent(this._content);
168
+ }
169
+ }
170
+
171
+ public getLogs(): string {
172
+ if (this.logs.length === 0) return i18n("No logs for {filename}").replace("{filename}", this.filename);
173
+ return this.logs.map((l) => `[${l.type}] ${l.text}`).join("\n");
174
+ }
175
+
176
+ override render() {
177
+ return html`
178
+ <div class="h-full flex flex-col">
179
+ <div class="flex-1 overflow-hidden relative">
180
+ <!-- Preview container - always in DOM, just hidden when not active -->
181
+ <div class="absolute inset-0 flex flex-col" style="display: ${this.viewMode === "preview" ? "flex" : "none"}">
182
+ <sandbox-iframe class="flex-1" ${ref(this.sandboxIframeRef)}></sandbox-iframe>
183
+ ${
184
+ this.logs.length > 0
185
+ ? html`<artifact-console .logs=${this.logs} ${ref(this.consoleRef)}></artifact-console>`
186
+ : ""
187
+ }
188
+ </div>
189
+
190
+ <!-- Code view - always in DOM, just hidden when not active -->
191
+ <div
192
+ class="absolute inset-0 overflow-auto bg-background"
193
+ style="display: ${this.viewMode === "code" ? "block" : "none"}"
194
+ >
195
+ <pre class="m-0 p-4 text-xs"><code class="hljs language-html">${unsafeHTML(
196
+ hljs.highlight(this._content, { language: "html" }).value,
197
+ )}</code></pre>
198
+ </div>
199
+ </div>
200
+ </div>
201
+ `;
202
+ }
203
+ }
@@ -0,0 +1,116 @@
1
+ import { DownloadButton } from "@mariozechner/mini-lit/dist/DownloadButton.js";
2
+ import { html, type TemplateResult } from "lit";
3
+ import { customElement, property } from "lit/decorators.js";
4
+ import { i18n } from "../../utils/i18n.js";
5
+ import { ArtifactElement } from "./ArtifactElement.js";
6
+
7
+ @customElement("image-artifact")
8
+ export class ImageArtifact extends ArtifactElement {
9
+ @property({ type: String }) private _content = "";
10
+
11
+ get content(): string {
12
+ return this._content;
13
+ }
14
+
15
+ set content(value: string) {
16
+ this._content = value;
17
+ this.requestUpdate();
18
+ }
19
+
20
+ protected override createRenderRoot(): HTMLElement | DocumentFragment {
21
+ return this;
22
+ }
23
+
24
+ override connectedCallback(): void {
25
+ super.connectedCallback();
26
+ this.style.display = "block";
27
+ this.style.height = "100%";
28
+ }
29
+
30
+ private getMimeType(): string {
31
+ const ext = this.filename.split(".").pop()?.toLowerCase();
32
+ if (ext === "jpg" || ext === "jpeg") return "image/jpeg";
33
+ if (ext === "gif") return "image/gif";
34
+ if (ext === "webp") return "image/webp";
35
+ if (ext === "svg") return "image/svg+xml";
36
+ if (ext === "bmp") return "image/bmp";
37
+ if (ext === "ico") return "image/x-icon";
38
+ return "image/png";
39
+ }
40
+
41
+ private getImageUrl(): string {
42
+ // If content is already a data URL, use it directly
43
+ if (this._content.startsWith("data:")) {
44
+ return this._content;
45
+ }
46
+ // Otherwise assume it's base64 and construct data URL
47
+ return `data:${this.getMimeType()};base64,${this._content}`;
48
+ }
49
+
50
+ private decodeBase64(): Uint8Array {
51
+ let base64Data: string;
52
+
53
+ // If content is a data URL, extract the base64 part
54
+ if (this._content.startsWith("data:")) {
55
+ const base64Match = this._content.match(/base64,(.+)/);
56
+ if (base64Match) {
57
+ base64Data = base64Match[1];
58
+ } else {
59
+ // Not a base64 data URL, return empty
60
+ return new Uint8Array(0);
61
+ }
62
+ } else {
63
+ // Otherwise use content as-is
64
+ base64Data = this._content;
65
+ }
66
+
67
+ // Decode base64 to binary string
68
+ const binaryString = atob(base64Data);
69
+
70
+ // Convert binary string to Uint8Array
71
+ const bytes = new Uint8Array(binaryString.length);
72
+ for (let i = 0; i < binaryString.length; i++) {
73
+ bytes[i] = binaryString.charCodeAt(i);
74
+ }
75
+
76
+ return bytes;
77
+ }
78
+
79
+ public getHeaderButtons() {
80
+ return html`
81
+ <div class="flex items-center gap-1">
82
+ ${DownloadButton({
83
+ content: this.decodeBase64(),
84
+ filename: this.filename,
85
+ mimeType: this.getMimeType(),
86
+ title: i18n("Download"),
87
+ })}
88
+ </div>
89
+ `;
90
+ }
91
+
92
+ override render(): TemplateResult {
93
+ return html`
94
+ <div class="h-full flex flex-col bg-background overflow-auto">
95
+ <div class="flex-1 flex items-center justify-center p-4">
96
+ <img
97
+ src="${this.getImageUrl()}"
98
+ alt="${this.filename}"
99
+ class="max-w-full max-h-full object-contain"
100
+ @error=${(e: Event) => {
101
+ const target = e.target as HTMLImageElement;
102
+ target.src =
103
+ "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'%3E%3Ctext x='50' y='50' text-anchor='middle' dominant-baseline='middle' fill='%23999'%3EImage Error%3C/text%3E%3C/svg%3E";
104
+ }}
105
+ />
106
+ </div>
107
+ </div>
108
+ `;
109
+ }
110
+ }
111
+
112
+ declare global {
113
+ interface HTMLElementTagNameMap {
114
+ "image-artifact": ImageArtifact;
115
+ }
116
+ }
@@ -0,0 +1,83 @@
1
+ import hljs from "highlight.js";
2
+ import { html } from "lit";
3
+ import { customElement, property, state } from "lit/decorators.js";
4
+ import { unsafeHTML } from "lit/directives/unsafe-html.js";
5
+ import { i18n } from "../../utils/i18n.js";
6
+ import "@mariozechner/mini-lit/dist/MarkdownBlock.js";
7
+ import { CopyButton } from "@mariozechner/mini-lit/dist/CopyButton.js";
8
+ import { DownloadButton } from "@mariozechner/mini-lit/dist/DownloadButton.js";
9
+ import { PreviewCodeToggle } from "@mariozechner/mini-lit/dist/PreviewCodeToggle.js";
10
+ import { ArtifactElement } from "./ArtifactElement.js";
11
+
12
+ @customElement("markdown-artifact")
13
+ export class MarkdownArtifact extends ArtifactElement {
14
+ @property() override filename = "";
15
+
16
+ private _content = "";
17
+ override get content(): string {
18
+ return this._content;
19
+ }
20
+ override set content(value: string) {
21
+ this._content = value;
22
+ this.requestUpdate();
23
+ }
24
+
25
+ @state() private viewMode: "preview" | "code" = "preview";
26
+
27
+ protected override createRenderRoot(): HTMLElement | DocumentFragment {
28
+ return this; // light DOM
29
+ }
30
+
31
+ private setViewMode(mode: "preview" | "code") {
32
+ this.viewMode = mode;
33
+ }
34
+
35
+ public getHeaderButtons() {
36
+ const toggle = new PreviewCodeToggle();
37
+ toggle.mode = this.viewMode;
38
+ toggle.addEventListener("mode-change", (e: Event) => {
39
+ this.setViewMode((e as CustomEvent).detail);
40
+ });
41
+
42
+ const copyButton = new CopyButton();
43
+ copyButton.text = this._content;
44
+ copyButton.title = i18n("Copy Markdown");
45
+ copyButton.showText = false;
46
+
47
+ return html`
48
+ <div class="flex items-center gap-2">
49
+ ${toggle} ${copyButton}
50
+ ${DownloadButton({
51
+ content: this._content,
52
+ filename: this.filename,
53
+ mimeType: "text/markdown",
54
+ title: i18n("Download Markdown"),
55
+ })}
56
+ </div>
57
+ `;
58
+ }
59
+
60
+ override render() {
61
+ return html`
62
+ <div class="h-full flex flex-col">
63
+ <div class="flex-1 overflow-auto">
64
+ ${
65
+ this.viewMode === "preview"
66
+ ? html`<div class="p-4"><markdown-block .content=${this.content}></markdown-block></div>`
67
+ : html`<pre
68
+ class="m-0 p-4 text-xs whitespace-pre-wrap break-words"
69
+ ><code class="hljs language-markdown">${unsafeHTML(
70
+ hljs.highlight(this.content, { language: "markdown", ignoreIllegals: true }).value,
71
+ )}</code></pre>`
72
+ }
73
+ </div>
74
+ </div>
75
+ `;
76
+ }
77
+ }
78
+
79
+ declare global {
80
+ interface HTMLElementTagNameMap {
81
+ "markdown-artifact": MarkdownArtifact;
82
+ }
83
+ }
@@ -0,0 +1,201 @@
1
+ import { DownloadButton } from "@mariozechner/mini-lit/dist/DownloadButton.js";
2
+ import { html, type TemplateResult } from "lit";
3
+ import { customElement, property, state } from "lit/decorators.js";
4
+ import * as pdfjsLib from "pdfjs-dist";
5
+ import { i18n } from "../../utils/i18n.js";
6
+ import { ArtifactElement } from "./ArtifactElement.js";
7
+
8
+ // Configure PDF.js worker
9
+ pdfjsLib.GlobalWorkerOptions.workerSrc = new URL("pdfjs-dist/build/pdf.worker.min.mjs", import.meta.url).toString();
10
+
11
+ @customElement("pdf-artifact")
12
+ export class PdfArtifact extends ArtifactElement {
13
+ @property({ type: String }) private _content = "";
14
+ @state() private error: string | null = null;
15
+ private currentLoadingTask: any = null;
16
+
17
+ get content(): string {
18
+ return this._content;
19
+ }
20
+
21
+ set content(value: string) {
22
+ this._content = value;
23
+ this.error = null;
24
+ this.requestUpdate();
25
+ }
26
+
27
+ protected override createRenderRoot(): HTMLElement | DocumentFragment {
28
+ return this;
29
+ }
30
+
31
+ override connectedCallback(): void {
32
+ super.connectedCallback();
33
+ this.style.display = "block";
34
+ this.style.height = "100%";
35
+ }
36
+
37
+ override disconnectedCallback(): void {
38
+ super.disconnectedCallback();
39
+ this.cleanup();
40
+ }
41
+
42
+ private cleanup() {
43
+ if (this.currentLoadingTask) {
44
+ this.currentLoadingTask.destroy();
45
+ this.currentLoadingTask = null;
46
+ }
47
+ }
48
+
49
+ private base64ToArrayBuffer(base64: string): ArrayBuffer {
50
+ // Remove data URL prefix if present
51
+ let base64Data = base64;
52
+ if (base64.startsWith("data:")) {
53
+ const base64Match = base64.match(/base64,(.+)/);
54
+ if (base64Match) {
55
+ base64Data = base64Match[1];
56
+ }
57
+ }
58
+
59
+ const binaryString = atob(base64Data);
60
+ const bytes = new Uint8Array(binaryString.length);
61
+ for (let i = 0; i < binaryString.length; i++) {
62
+ bytes[i] = binaryString.charCodeAt(i);
63
+ }
64
+ return bytes.buffer;
65
+ }
66
+
67
+ private decodeBase64(): Uint8Array {
68
+ let base64Data = this._content;
69
+ if (this._content.startsWith("data:")) {
70
+ const base64Match = this._content.match(/base64,(.+)/);
71
+ if (base64Match) {
72
+ base64Data = base64Match[1];
73
+ }
74
+ }
75
+
76
+ const binaryString = atob(base64Data);
77
+ const bytes = new Uint8Array(binaryString.length);
78
+ for (let i = 0; i < binaryString.length; i++) {
79
+ bytes[i] = binaryString.charCodeAt(i);
80
+ }
81
+ return bytes;
82
+ }
83
+
84
+ public getHeaderButtons() {
85
+ return html`
86
+ <div class="flex items-center gap-1">
87
+ ${DownloadButton({
88
+ content: this.decodeBase64(),
89
+ filename: this.filename,
90
+ mimeType: "application/pdf",
91
+ title: i18n("Download"),
92
+ })}
93
+ </div>
94
+ `;
95
+ }
96
+
97
+ override async updated(changedProperties: Map<string, any>) {
98
+ super.updated(changedProperties);
99
+
100
+ if (changedProperties.has("_content") && this._content && !this.error) {
101
+ await this.renderPdf();
102
+ }
103
+ }
104
+
105
+ private async renderPdf() {
106
+ const container = this.querySelector("#pdf-container");
107
+ if (!container || !this._content) return;
108
+
109
+ let pdf: any = null;
110
+
111
+ try {
112
+ const arrayBuffer = this.base64ToArrayBuffer(this._content);
113
+
114
+ // Cancel any existing loading task
115
+ if (this.currentLoadingTask) {
116
+ this.currentLoadingTask.destroy();
117
+ }
118
+
119
+ // Load the PDF
120
+ this.currentLoadingTask = pdfjsLib.getDocument({ data: arrayBuffer });
121
+ pdf = await this.currentLoadingTask.promise;
122
+ this.currentLoadingTask = null;
123
+
124
+ // Clear container
125
+ container.innerHTML = "";
126
+ const wrapper = document.createElement("div");
127
+ wrapper.className = "p-4";
128
+ container.appendChild(wrapper);
129
+
130
+ // Render all pages
131
+ for (let pageNum = 1; pageNum <= pdf.numPages; pageNum++) {
132
+ const page = await pdf.getPage(pageNum);
133
+
134
+ const pageContainer = document.createElement("div");
135
+ pageContainer.className = "mb-4 last:mb-0";
136
+
137
+ const canvas = document.createElement("canvas");
138
+ const context = canvas.getContext("2d");
139
+
140
+ const viewport = page.getViewport({ scale: 1.5 });
141
+ canvas.height = viewport.height;
142
+ canvas.width = viewport.width;
143
+
144
+ canvas.className = "w-full max-w-full h-auto block mx-auto bg-white rounded shadow-sm border border-border";
145
+
146
+ if (context) {
147
+ context.fillStyle = "white";
148
+ context.fillRect(0, 0, canvas.width, canvas.height);
149
+ }
150
+
151
+ await page.render({
152
+ canvasContext: context!,
153
+ viewport: viewport,
154
+ canvas: canvas,
155
+ }).promise;
156
+
157
+ pageContainer.appendChild(canvas);
158
+
159
+ if (pageNum < pdf.numPages) {
160
+ const separator = document.createElement("div");
161
+ separator.className = "h-px bg-border my-4";
162
+ pageContainer.appendChild(separator);
163
+ }
164
+
165
+ wrapper.appendChild(pageContainer);
166
+ }
167
+ } catch (error: any) {
168
+ console.error("Error rendering PDF:", error);
169
+ this.error = error?.message || i18n("Failed to load PDF");
170
+ } finally {
171
+ if (pdf) {
172
+ pdf.destroy();
173
+ }
174
+ }
175
+ }
176
+
177
+ override render(): TemplateResult {
178
+ if (this.error) {
179
+ return html`
180
+ <div class="h-full flex items-center justify-center bg-background p-4">
181
+ <div class="bg-destructive/10 border border-destructive text-destructive p-4 rounded-lg max-w-2xl">
182
+ <div class="font-medium mb-1">${i18n("Error loading PDF")}</div>
183
+ <div class="text-sm opacity-90">${this.error}</div>
184
+ </div>
185
+ </div>
186
+ `;
187
+ }
188
+
189
+ return html`
190
+ <div class="h-full flex flex-col bg-background overflow-auto">
191
+ <div id="pdf-container" class="flex-1 overflow-auto"></div>
192
+ </div>
193
+ `;
194
+ }
195
+ }
196
+
197
+ declare global {
198
+ interface HTMLElementTagNameMap {
199
+ "pdf-artifact": PdfArtifact;
200
+ }
201
+ }
@@ -0,0 +1,82 @@
1
+ import { CopyButton } from "@mariozechner/mini-lit/dist/CopyButton.js";
2
+ import { DownloadButton } from "@mariozechner/mini-lit/dist/DownloadButton.js";
3
+ import { PreviewCodeToggle } from "@mariozechner/mini-lit/dist/PreviewCodeToggle.js";
4
+ import hljs from "highlight.js";
5
+ import { html } from "lit";
6
+ import { customElement, property, state } from "lit/decorators.js";
7
+ import { unsafeHTML } from "lit/directives/unsafe-html.js";
8
+ import { i18n } from "../../utils/i18n.js";
9
+ import { ArtifactElement } from "./ArtifactElement.js";
10
+
11
+ @customElement("svg-artifact")
12
+ export class SvgArtifact extends ArtifactElement {
13
+ @property() override filename = "";
14
+
15
+ private _content = "";
16
+ override get content(): string {
17
+ return this._content;
18
+ }
19
+ override set content(value: string) {
20
+ this._content = value;
21
+ this.requestUpdate();
22
+ }
23
+
24
+ @state() private viewMode: "preview" | "code" = "preview";
25
+
26
+ protected override createRenderRoot(): HTMLElement | DocumentFragment {
27
+ return this; // light DOM
28
+ }
29
+
30
+ private setViewMode(mode: "preview" | "code") {
31
+ this.viewMode = mode;
32
+ }
33
+
34
+ public getHeaderButtons() {
35
+ const toggle = new PreviewCodeToggle();
36
+ toggle.mode = this.viewMode;
37
+ toggle.addEventListener("mode-change", (e: Event) => {
38
+ this.setViewMode((e as CustomEvent).detail);
39
+ });
40
+
41
+ const copyButton = new CopyButton();
42
+ copyButton.text = this._content;
43
+ copyButton.title = i18n("Copy SVG");
44
+ copyButton.showText = false;
45
+
46
+ return html`
47
+ <div class="flex items-center gap-2">
48
+ ${toggle} ${copyButton}
49
+ ${DownloadButton({
50
+ content: this._content,
51
+ filename: this.filename,
52
+ mimeType: "image/svg+xml",
53
+ title: i18n("Download SVG"),
54
+ })}
55
+ </div>
56
+ `;
57
+ }
58
+
59
+ override render() {
60
+ return html`
61
+ <div class="h-full flex flex-col">
62
+ <div class="flex-1 overflow-auto">
63
+ ${
64
+ this.viewMode === "preview"
65
+ ? html`<div class="h-full flex items-center justify-center">
66
+ ${unsafeHTML(this.content.replace(/<svg(\s|>)/i, (_m, p1) => `<svg class="w-full h-full"${p1}`))}
67
+ </div>`
68
+ : html`<pre class="m-0 p-4 text-xs"><code class="hljs language-xml">${unsafeHTML(
69
+ hljs.highlight(this.content, { language: "xml", ignoreIllegals: true }).value,
70
+ )}</code></pre>`
71
+ }
72
+ </div>
73
+ </div>
74
+ `;
75
+ }
76
+ }
77
+
78
+ declare global {
79
+ interface HTMLElementTagNameMap {
80
+ "svg-artifact": SvgArtifact;
81
+ }
82
+ }