@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,148 @@
1
+ import { CopyButton } from "@mariozechner/mini-lit/dist/CopyButton.js";
2
+ import { DownloadButton } from "@mariozechner/mini-lit/dist/DownloadButton.js";
3
+ import hljs from "highlight.js";
4
+ import { html } from "lit";
5
+ import { customElement, property } from "lit/decorators.js";
6
+ import { unsafeHTML } from "lit/directives/unsafe-html.js";
7
+ import { i18n } from "../../utils/i18n.js";
8
+ import { ArtifactElement } from "./ArtifactElement.js";
9
+
10
+ // Known code file extensions for highlighting
11
+ const CODE_EXTENSIONS = [
12
+ "js",
13
+ "javascript",
14
+ "ts",
15
+ "typescript",
16
+ "jsx",
17
+ "tsx",
18
+ "py",
19
+ "python",
20
+ "java",
21
+ "c",
22
+ "cpp",
23
+ "cs",
24
+ "php",
25
+ "rb",
26
+ "ruby",
27
+ "go",
28
+ "rust",
29
+ "swift",
30
+ "kotlin",
31
+ "scala",
32
+ "dart",
33
+ "html",
34
+ "css",
35
+ "scss",
36
+ "sass",
37
+ "less",
38
+ "json",
39
+ "xml",
40
+ "yaml",
41
+ "yml",
42
+ "toml",
43
+ "sql",
44
+ "sh",
45
+ "bash",
46
+ "ps1",
47
+ "bat",
48
+ "r",
49
+ "matlab",
50
+ "julia",
51
+ "lua",
52
+ "perl",
53
+ "vue",
54
+ "svelte",
55
+ ];
56
+
57
+ @customElement("text-artifact")
58
+ export class TextArtifact extends ArtifactElement {
59
+ @property() override filename = "";
60
+
61
+ private _content = "";
62
+ override get content(): string {
63
+ return this._content;
64
+ }
65
+ override set content(value: string) {
66
+ this._content = value;
67
+ this.requestUpdate();
68
+ }
69
+
70
+ protected override createRenderRoot(): HTMLElement | DocumentFragment {
71
+ return this; // light DOM
72
+ }
73
+
74
+ private isCode(): boolean {
75
+ const ext = this.filename.split(".").pop()?.toLowerCase() || "";
76
+ return CODE_EXTENSIONS.includes(ext);
77
+ }
78
+
79
+ private getLanguageFromExtension(ext: string): string {
80
+ const languageMap: Record<string, string> = {
81
+ js: "javascript",
82
+ ts: "typescript",
83
+ py: "python",
84
+ rb: "ruby",
85
+ yml: "yaml",
86
+ ps1: "powershell",
87
+ bat: "batch",
88
+ };
89
+ return languageMap[ext] || ext;
90
+ }
91
+
92
+ private getMimeType(): string {
93
+ const ext = this.filename.split(".").pop()?.toLowerCase() || "";
94
+ if (ext === "svg") return "image/svg+xml";
95
+ if (ext === "md" || ext === "markdown") return "text/markdown";
96
+ return "text/plain";
97
+ }
98
+
99
+ public getHeaderButtons() {
100
+ const copyButton = new CopyButton();
101
+ copyButton.text = this.content;
102
+ copyButton.title = i18n("Copy");
103
+ copyButton.showText = false;
104
+
105
+ return html`
106
+ <div class="flex items-center gap-1">
107
+ ${copyButton}
108
+ ${DownloadButton({
109
+ content: this.content,
110
+ filename: this.filename,
111
+ mimeType: this.getMimeType(),
112
+ title: i18n("Download"),
113
+ })}
114
+ </div>
115
+ `;
116
+ }
117
+
118
+ override render() {
119
+ const isCode = this.isCode();
120
+ const ext = this.filename.split(".").pop() || "";
121
+ return html`
122
+ <div class="h-full flex flex-col">
123
+ <div class="flex-1 overflow-auto">
124
+ ${
125
+ isCode
126
+ ? html`
127
+ <pre class="m-0 p-4 text-xs"><code class="hljs language-${this.getLanguageFromExtension(
128
+ ext.toLowerCase(),
129
+ )}">${unsafeHTML(
130
+ hljs.highlight(this.content, {
131
+ language: this.getLanguageFromExtension(ext.toLowerCase()),
132
+ ignoreIllegals: true,
133
+ }).value,
134
+ )}</code></pre>
135
+ `
136
+ : html` <pre class="m-0 p-4 text-xs font-mono">${this.content}</pre> `
137
+ }
138
+ </div>
139
+ </div>
140
+ `;
141
+ }
142
+ }
143
+
144
+ declare global {
145
+ interface HTMLElementTagNameMap {
146
+ "text-artifact": TextArtifact;
147
+ }
148
+ }
@@ -0,0 +1,371 @@
1
+ import "@mariozechner/mini-lit/dist/CodeBlock.js";
2
+ import type { ToolResultMessage } from "@oh-my-pi/pi-ai";
3
+ import { createRef, ref } from "lit/directives/ref.js";
4
+ import { FileCode2 } from "lucide";
5
+ import "../../components/ConsoleBlock.js";
6
+ import { Diff } from "@mariozechner/mini-lit/dist/Diff.js";
7
+ import { html, type TemplateResult } from "lit";
8
+ import { i18n } from "../../utils/i18n.js";
9
+ import { renderCollapsibleHeader, renderHeader } from "../renderer-registry.js";
10
+ import type { ToolRenderer, ToolRenderResult } from "../types.js";
11
+ import { ArtifactPill } from "./ArtifactPill.js";
12
+ import type { ArtifactsPanel, ArtifactsParams } from "./artifacts.js";
13
+
14
+ // Helper to extract text from content blocks
15
+ function getTextOutput(result: ToolResultMessage<any> | undefined): string {
16
+ if (!result) return "";
17
+ return (
18
+ result.content
19
+ ?.filter((c) => c.type === "text")
20
+ .map((c: any) => c.text)
21
+ .join("\n") || ""
22
+ );
23
+ }
24
+
25
+ // Helper to determine language for syntax highlighting
26
+ function getLanguageFromFilename(filename?: string): string {
27
+ if (!filename) return "text";
28
+ const ext = filename.split(".").pop()?.toLowerCase();
29
+ const languageMap: Record<string, string> = {
30
+ js: "javascript",
31
+ jsx: "javascript",
32
+ ts: "typescript",
33
+ tsx: "typescript",
34
+ html: "html",
35
+ css: "css",
36
+ scss: "scss",
37
+ json: "json",
38
+ py: "python",
39
+ md: "markdown",
40
+ svg: "xml",
41
+ xml: "xml",
42
+ yaml: "yaml",
43
+ yml: "yaml",
44
+ sh: "bash",
45
+ bash: "bash",
46
+ sql: "sql",
47
+ java: "java",
48
+ c: "c",
49
+ cpp: "cpp",
50
+ cs: "csharp",
51
+ go: "go",
52
+ rs: "rust",
53
+ php: "php",
54
+ rb: "ruby",
55
+ swift: "swift",
56
+ kt: "kotlin",
57
+ r: "r",
58
+ };
59
+ return languageMap[ext || ""] || "text";
60
+ }
61
+
62
+ export class ArtifactsToolRenderer implements ToolRenderer<ArtifactsParams, undefined> {
63
+ constructor(public artifactsPanel?: ArtifactsPanel) {}
64
+
65
+ render(
66
+ params: ArtifactsParams | undefined,
67
+ result: ToolResultMessage<undefined> | undefined,
68
+ isStreaming?: boolean,
69
+ ): ToolRenderResult {
70
+ const state = result ? (result.isError ? "error" : "complete") : isStreaming ? "inprogress" : "complete";
71
+
72
+ // Create refs for collapsible sections
73
+ const contentRef = createRef<HTMLDivElement>();
74
+ const chevronRef = createRef<HTMLSpanElement>();
75
+
76
+ // Helper to get command labels
77
+ const getCommandLabels = (command: string): { streaming: string; complete: string } => {
78
+ const labels: Record<string, { streaming: string; complete: string }> = {
79
+ create: { streaming: i18n("Creating artifact"), complete: i18n("Created artifact") },
80
+ update: { streaming: i18n("Updating artifact"), complete: i18n("Updated artifact") },
81
+ rewrite: { streaming: i18n("Rewriting artifact"), complete: i18n("Rewrote artifact") },
82
+ get: { streaming: i18n("Getting artifact"), complete: i18n("Got artifact") },
83
+ delete: { streaming: i18n("Deleting artifact"), complete: i18n("Deleted artifact") },
84
+ logs: { streaming: i18n("Getting logs"), complete: i18n("Got logs") },
85
+ };
86
+ return labels[command] || { streaming: i18n("Processing artifact"), complete: i18n("Processed artifact") };
87
+ };
88
+
89
+ // Helper to render header text with inline artifact pill
90
+ const renderHeaderWithPill = (labelText: string, filename?: string): TemplateResult => {
91
+ if (filename) {
92
+ return html`<span>${labelText} ${ArtifactPill(filename, this.artifactsPanel)}</span>`;
93
+ }
94
+ return html`<span>${labelText}</span>`;
95
+ };
96
+
97
+ // Error handling
98
+ if (result?.isError) {
99
+ const command = params?.command;
100
+ const filename = params?.filename;
101
+ const labels = command
102
+ ? getCommandLabels(command)
103
+ : { streaming: i18n("Processing artifact"), complete: i18n("Processed artifact") };
104
+ const headerText = labels.streaming;
105
+
106
+ // For create/update/rewrite errors, show code block + console/error
107
+ if (command === "create" || command === "update" || command === "rewrite") {
108
+ const content = params?.content || "";
109
+ const { old_str, new_str } = params || {};
110
+ const isDiff = command === "update";
111
+ const diffContent =
112
+ old_str !== undefined && new_str !== undefined ? Diff({ oldText: old_str, newText: new_str }) : "";
113
+
114
+ const isHtml = filename?.endsWith(".html");
115
+
116
+ return {
117
+ content: html`
118
+ <div>
119
+ ${renderCollapsibleHeader(
120
+ state,
121
+ FileCode2,
122
+ renderHeaderWithPill(headerText, filename),
123
+ contentRef,
124
+ chevronRef,
125
+ false,
126
+ )}
127
+ <div ${ref(contentRef)} class="max-h-0 overflow-hidden transition-all duration-300 space-y-3">
128
+ ${
129
+ isDiff
130
+ ? diffContent
131
+ : content
132
+ ? html`<code-block .code=${content} language=${getLanguageFromFilename(filename)}></code-block>`
133
+ : ""
134
+ }
135
+ ${
136
+ isHtml
137
+ ? html`<console-block
138
+ .content=${getTextOutput(result) || i18n("An error occurred")}
139
+ variant="error"
140
+ ></console-block>`
141
+ : html`<div class="text-sm text-destructive">
142
+ ${getTextOutput(result) || i18n("An error occurred")}
143
+ </div>`
144
+ }
145
+ </div>
146
+ </div>
147
+ `,
148
+ isCustom: false,
149
+ };
150
+ }
151
+
152
+ // For other errors, just show error message
153
+ return {
154
+ content: html`
155
+ <div class="space-y-3">
156
+ ${renderHeader(state, FileCode2, headerText)}
157
+ <div class="text-sm text-destructive">${getTextOutput(result) || i18n("An error occurred")}</div>
158
+ </div>
159
+ `,
160
+ isCustom: false,
161
+ };
162
+ }
163
+
164
+ // Full params + result
165
+ if (result && params) {
166
+ const { command, filename, content } = params;
167
+ const labels = command
168
+ ? getCommandLabels(command)
169
+ : { streaming: i18n("Processing artifact"), complete: i18n("Processed artifact") };
170
+ const headerText = labels.complete;
171
+
172
+ // GET command: show code block with file content
173
+ if (command === "get") {
174
+ const fileContent = getTextOutput(result) || i18n("(no output)");
175
+ return {
176
+ content: html`
177
+ <div>
178
+ ${renderCollapsibleHeader(
179
+ state,
180
+ FileCode2,
181
+ renderHeaderWithPill(headerText, filename),
182
+ contentRef,
183
+ chevronRef,
184
+ false,
185
+ )}
186
+ <div ${ref(contentRef)} class="max-h-0 overflow-hidden transition-all duration-300">
187
+ <code-block .code=${fileContent} language=${getLanguageFromFilename(filename)}></code-block>
188
+ </div>
189
+ </div>
190
+ `,
191
+ isCustom: false,
192
+ };
193
+ }
194
+
195
+ // LOGS command: show console block
196
+ if (command === "logs") {
197
+ const logs = getTextOutput(result) || i18n("(no output)");
198
+ return {
199
+ content: html`
200
+ <div>
201
+ ${renderCollapsibleHeader(
202
+ state,
203
+ FileCode2,
204
+ renderHeaderWithPill(headerText, filename),
205
+ contentRef,
206
+ chevronRef,
207
+ false,
208
+ )}
209
+ <div ${ref(contentRef)} class="max-h-0 overflow-hidden transition-all duration-300">
210
+ <console-block .content=${logs}></console-block>
211
+ </div>
212
+ </div>
213
+ `,
214
+ isCustom: false,
215
+ };
216
+ }
217
+
218
+ // CREATE/UPDATE/REWRITE: always show code block, + console block for .html files
219
+ if (command === "create" || command === "rewrite") {
220
+ const codeContent = content || "";
221
+ const isHtml = filename?.endsWith(".html");
222
+ const logs = getTextOutput(result) || "";
223
+
224
+ return {
225
+ content: html`
226
+ <div>
227
+ ${renderCollapsibleHeader(
228
+ state,
229
+ FileCode2,
230
+ renderHeaderWithPill(headerText, filename),
231
+ contentRef,
232
+ chevronRef,
233
+ false,
234
+ )}
235
+ <div ${ref(contentRef)} class="max-h-0 overflow-hidden transition-all duration-300 space-y-3">
236
+ ${
237
+ codeContent
238
+ ? html`<code-block .code=${codeContent} language=${getLanguageFromFilename(filename)}></code-block>`
239
+ : ""
240
+ }
241
+ ${isHtml && logs ? html`<console-block .content=${logs}></console-block>` : ""}
242
+ </div>
243
+ </div>
244
+ `,
245
+ isCustom: false,
246
+ };
247
+ }
248
+
249
+ if (command === "update") {
250
+ const isHtml = filename?.endsWith(".html");
251
+ const logs = getTextOutput(result) || "";
252
+ return {
253
+ content: html`
254
+ <div>
255
+ ${renderCollapsibleHeader(
256
+ state,
257
+ FileCode2,
258
+ renderHeaderWithPill(headerText, filename),
259
+ contentRef,
260
+ chevronRef,
261
+ false,
262
+ )}
263
+ <div ${ref(contentRef)} class="max-h-0 overflow-hidden transition-all duration-300 space-y-3">
264
+ ${Diff({ oldText: params.old_str || "", newText: params.new_str || "" })}
265
+ ${isHtml && logs ? html`<console-block .content=${logs}></console-block>` : ""}
266
+ </div>
267
+ </div>
268
+ `,
269
+ isCustom: false,
270
+ };
271
+ }
272
+
273
+ // For DELETE, just show header
274
+ return {
275
+ content: html`
276
+ <div class="space-y-3">${renderHeader(state, FileCode2, renderHeaderWithPill(headerText, filename))}</div>
277
+ `,
278
+ isCustom: false,
279
+ };
280
+ }
281
+
282
+ // Params only (streaming or waiting for result)
283
+ if (params) {
284
+ const { command, filename, content, old_str, new_str } = params;
285
+
286
+ // If no command yet
287
+ if (!command) {
288
+ return { content: renderHeader(state, FileCode2, i18n("Preparing artifact...")), isCustom: false };
289
+ }
290
+
291
+ const labels = getCommandLabels(command);
292
+ const headerText = labels.streaming;
293
+
294
+ // Render based on command type
295
+ switch (command) {
296
+ case "create":
297
+ case "rewrite":
298
+ return {
299
+ content: html`
300
+ <div>
301
+ ${renderCollapsibleHeader(
302
+ state,
303
+ FileCode2,
304
+ renderHeaderWithPill(headerText, filename),
305
+ contentRef,
306
+ chevronRef,
307
+ false,
308
+ )}
309
+ <div ${ref(contentRef)} class="max-h-0 overflow-hidden transition-all duration-300">
310
+ ${
311
+ content
312
+ ? html`<code-block .code=${content} language=${getLanguageFromFilename(filename)}></code-block>`
313
+ : ""
314
+ }
315
+ </div>
316
+ </div>
317
+ `,
318
+ isCustom: false,
319
+ };
320
+
321
+ case "update":
322
+ return {
323
+ content: html`
324
+ <div>
325
+ ${renderCollapsibleHeader(
326
+ state,
327
+ FileCode2,
328
+ renderHeaderWithPill(headerText, filename),
329
+ contentRef,
330
+ chevronRef,
331
+ false,
332
+ )}
333
+ <div ${ref(contentRef)} class="max-h-0 overflow-hidden transition-all duration-300">
334
+ ${old_str !== undefined && new_str !== undefined ? Diff({ oldText: old_str, newText: new_str }) : ""}
335
+ </div>
336
+ </div>
337
+ `,
338
+ isCustom: false,
339
+ };
340
+
341
+ case "get":
342
+ case "logs":
343
+ return {
344
+ content: html`
345
+ <div>
346
+ ${renderCollapsibleHeader(
347
+ state,
348
+ FileCode2,
349
+ renderHeaderWithPill(headerText, filename),
350
+ contentRef,
351
+ chevronRef,
352
+ false,
353
+ )}
354
+ <div ${ref(contentRef)} class="max-h-0 overflow-hidden transition-all duration-300"></div>
355
+ </div>
356
+ `,
357
+ isCustom: false,
358
+ };
359
+
360
+ default:
361
+ return {
362
+ content: html` <div>${renderHeader(state, FileCode2, renderHeaderWithPill(headerText, filename))}</div> `,
363
+ isCustom: false,
364
+ };
365
+ }
366
+ }
367
+
368
+ // No params or result yet
369
+ return { content: renderHeader(state, FileCode2, i18n("Preparing artifact...")), isCustom: false };
370
+ }
371
+ }