@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,127 @@
1
+ import { icon } from "@mariozechner/mini-lit";
2
+ import { html, type TemplateResult } from "lit";
3
+ import type { Ref } from "lit/directives/ref.js";
4
+ import { ref } from "lit/directives/ref.js";
5
+ import { ChevronsUpDown, ChevronUp, Loader } from "lucide";
6
+ import type { ToolRenderer } from "./types.js";
7
+
8
+ // Registry of tool renderers
9
+ export const toolRenderers = new Map<string, ToolRenderer>();
10
+
11
+ /**
12
+ * Register a custom tool renderer
13
+ */
14
+ export function registerToolRenderer(toolName: string, renderer: ToolRenderer): void {
15
+ toolRenderers.set(toolName, renderer);
16
+ }
17
+
18
+ /**
19
+ * Get a tool renderer by name
20
+ */
21
+ export function getToolRenderer(toolName: string): ToolRenderer | undefined {
22
+ return toolRenderers.get(toolName);
23
+ }
24
+
25
+ /**
26
+ * Helper to render a header for tool renderers
27
+ * Shows icon on left when complete/error, spinner on right when in progress
28
+ */
29
+ export function renderHeader(
30
+ state: "inprogress" | "complete" | "error",
31
+ toolIcon: any,
32
+ text: string | TemplateResult,
33
+ ): TemplateResult {
34
+ const statusIcon = (iconComponent: any, color: string) =>
35
+ html`<span class="inline-block ${color}">${icon(iconComponent, "sm")}</span>`;
36
+
37
+ switch (state) {
38
+ case "inprogress":
39
+ return html`
40
+ <div class="flex items-center justify-between gap-2 text-sm text-muted-foreground">
41
+ <div class="flex items-center gap-2">${statusIcon(toolIcon, "text-foreground")} ${text}</div>
42
+ ${statusIcon(Loader, "text-foreground animate-spin")}
43
+ </div>
44
+ `;
45
+ case "complete":
46
+ return html`
47
+ <div class="flex items-center gap-2 text-sm text-muted-foreground">
48
+ ${statusIcon(toolIcon, "text-green-600 dark:text-green-500")} ${text}
49
+ </div>
50
+ `;
51
+ case "error":
52
+ return html`
53
+ <div class="flex items-center gap-2 text-sm text-muted-foreground">
54
+ ${statusIcon(toolIcon, "text-destructive")} ${text}
55
+ </div>
56
+ `;
57
+ }
58
+ }
59
+
60
+ /**
61
+ * Helper to render a collapsible header for tool renderers
62
+ * Same as renderHeader but with a chevron button that toggles visibility of content
63
+ */
64
+ export function renderCollapsibleHeader(
65
+ state: "inprogress" | "complete" | "error",
66
+ toolIcon: any,
67
+ text: string | TemplateResult,
68
+ contentRef: Ref<HTMLElement>,
69
+ chevronRef: Ref<HTMLElement>,
70
+ defaultExpanded = false,
71
+ ): TemplateResult {
72
+ const statusIcon = (iconComponent: any, color: string) =>
73
+ html`<span class="inline-block ${color}">${icon(iconComponent, "sm")}</span>`;
74
+
75
+ const toggleContent = (e: Event) => {
76
+ e.preventDefault();
77
+ const content = contentRef.value;
78
+ const chevron = chevronRef.value;
79
+ if (content && chevron) {
80
+ const isCollapsed = content.classList.contains("max-h-0");
81
+ if (isCollapsed) {
82
+ content.classList.remove("max-h-0");
83
+ content.classList.add("max-h-[2000px]", "mt-3");
84
+ // Show ChevronUp, hide ChevronsUpDown
85
+ const upIcon = chevron.querySelector(".chevron-up");
86
+ const downIcon = chevron.querySelector(".chevrons-up-down");
87
+ if (upIcon && downIcon) {
88
+ upIcon.classList.remove("hidden");
89
+ downIcon.classList.add("hidden");
90
+ }
91
+ } else {
92
+ content.classList.remove("max-h-[2000px]", "mt-3");
93
+ content.classList.add("max-h-0");
94
+ // Show ChevronsUpDown, hide ChevronUp
95
+ const upIcon = chevron.querySelector(".chevron-up");
96
+ const downIcon = chevron.querySelector(".chevrons-up-down");
97
+ if (upIcon && downIcon) {
98
+ upIcon.classList.add("hidden");
99
+ downIcon.classList.remove("hidden");
100
+ }
101
+ }
102
+ }
103
+ };
104
+
105
+ const toolIconColor =
106
+ state === "complete"
107
+ ? "text-green-600 dark:text-green-500"
108
+ : state === "error"
109
+ ? "text-destructive"
110
+ : "text-foreground";
111
+
112
+ return html`
113
+ <button
114
+ @click=${toggleContent}
115
+ class="flex items-center justify-between gap-2 text-sm text-muted-foreground w-full text-left hover:text-foreground transition-colors cursor-pointer"
116
+ >
117
+ <div class="flex items-center gap-2">
118
+ ${state === "inprogress" ? statusIcon(Loader, "text-foreground animate-spin") : ""}
119
+ ${statusIcon(toolIcon, toolIconColor)} ${text}
120
+ </div>
121
+ <span class="inline-block text-muted-foreground" ${ref(chevronRef)}>
122
+ <span class="chevron-up ${defaultExpanded ? "" : "hidden"}">${icon(ChevronUp, "sm")}</span>
123
+ <span class="chevrons-up-down ${defaultExpanded ? "hidden" : ""}">${icon(ChevronsUpDown, "sm")}</span>
124
+ </span>
125
+ </button>
126
+ `;
127
+ }
@@ -0,0 +1,52 @@
1
+ import type { ToolResultMessage } from "@oh-my-pi/pi-ai";
2
+ import { html } from "lit";
3
+ import { SquareTerminal } from "lucide";
4
+ import { i18n } from "../../utils/i18n.js";
5
+ import { renderHeader } from "../renderer-registry.js";
6
+ import type { ToolRenderer, ToolRenderResult } from "../types.js";
7
+
8
+ interface BashParams {
9
+ command: string;
10
+ }
11
+
12
+ // Bash tool has undefined details (only uses output)
13
+ export class BashRenderer implements ToolRenderer<BashParams, undefined> {
14
+ render(params: BashParams | undefined, result: ToolResultMessage<undefined> | undefined): ToolRenderResult {
15
+ const state = result ? (result.isError ? "error" : "complete") : "inprogress";
16
+
17
+ // With result: show command + output
18
+ if (result && params?.command) {
19
+ const output =
20
+ result.content
21
+ ?.filter((c) => c.type === "text")
22
+ .map((c: any) => c.text)
23
+ .join("\n") || "";
24
+ const combined = output ? `> ${params.command}\n\n${output}` : `> ${params.command}`;
25
+ return {
26
+ content: html`
27
+ <div class="space-y-3">
28
+ ${renderHeader(state, SquareTerminal, i18n("Running command..."))}
29
+ <console-block .content=${combined} .variant=${result.isError ? "error" : "default"}></console-block>
30
+ </div>
31
+ `,
32
+ isCustom: false,
33
+ };
34
+ }
35
+
36
+ // Just params (streaming or waiting)
37
+ if (params?.command) {
38
+ return {
39
+ content: html`
40
+ <div class="space-y-3">
41
+ ${renderHeader(state, SquareTerminal, i18n("Running command..."))}
42
+ <console-block .content=${`> ${params.command}`}></console-block>
43
+ </div>
44
+ `,
45
+ isCustom: false,
46
+ };
47
+ }
48
+
49
+ // No params yet
50
+ return { content: renderHeader(state, SquareTerminal, i18n("Waiting for command...")), isCustom: false };
51
+ }
52
+ }
@@ -0,0 +1,58 @@
1
+ import type { ToolResultMessage } from "@oh-my-pi/pi-ai";
2
+ import { html } from "lit";
3
+ import { Calculator } from "lucide";
4
+ import { i18n } from "../../utils/i18n.js";
5
+ import { renderHeader } from "../renderer-registry.js";
6
+ import type { ToolRenderer, ToolRenderResult } from "../types.js";
7
+
8
+ interface CalculateParams {
9
+ expression: string;
10
+ }
11
+
12
+ // Calculate tool has undefined details (only uses output)
13
+ export class CalculateRenderer implements ToolRenderer<CalculateParams, undefined> {
14
+ render(params: CalculateParams | undefined, result: ToolResultMessage<undefined> | undefined): ToolRenderResult {
15
+ const state = result ? (result.isError ? "error" : "complete") : "inprogress";
16
+
17
+ // Full params + full result
18
+ if (result && params?.expression) {
19
+ const output =
20
+ result.content
21
+ ?.filter((c) => c.type === "text")
22
+ .map((c: any) => c.text)
23
+ .join("\n") || "";
24
+
25
+ // Error: show expression in header, error below
26
+ if (result.isError) {
27
+ return {
28
+ content: html`
29
+ <div class="space-y-3">
30
+ ${renderHeader(state, Calculator, params.expression)}
31
+ <div class="text-sm text-destructive">${output}</div>
32
+ </div>
33
+ `,
34
+ isCustom: false,
35
+ };
36
+ }
37
+
38
+ // Success: show expression = result in header
39
+ return { content: renderHeader(state, Calculator, `${params.expression} = ${output}`), isCustom: false };
40
+ }
41
+
42
+ // Full params, no result: just show header with expression in it
43
+ if (params?.expression) {
44
+ return {
45
+ content: renderHeader(state, Calculator, `${i18n("Calculating")} ${params.expression}`),
46
+ isCustom: false,
47
+ };
48
+ }
49
+
50
+ // Partial params (empty expression), no result
51
+ if (params && !params.expression) {
52
+ return { content: renderHeader(state, Calculator, i18n("Writing expression...")), isCustom: false };
53
+ }
54
+
55
+ // No params, no result
56
+ return { content: renderHeader(state, Calculator, i18n("Waiting for expression...")), isCustom: false };
57
+ }
58
+ }
@@ -0,0 +1,95 @@
1
+ import type { ToolResultMessage } from "@oh-my-pi/pi-ai";
2
+ import { html } from "lit";
3
+ import { Code } from "lucide";
4
+ import { i18n } from "../../utils/i18n.js";
5
+ import { renderHeader } from "../renderer-registry.js";
6
+ import type { ToolRenderer, ToolRenderResult } from "../types.js";
7
+
8
+ export class DefaultRenderer implements ToolRenderer {
9
+ render(params: any | undefined, result: ToolResultMessage | undefined, isStreaming?: boolean): ToolRenderResult {
10
+ const state = result ? (result.isError ? "error" : "complete") : isStreaming ? "inprogress" : "complete";
11
+
12
+ // Format params as JSON
13
+ let paramsJson = "";
14
+ if (params) {
15
+ try {
16
+ paramsJson = JSON.stringify(JSON.parse(params), null, 2);
17
+ } catch {
18
+ try {
19
+ paramsJson = JSON.stringify(params, null, 2);
20
+ } catch {
21
+ paramsJson = String(params);
22
+ }
23
+ }
24
+ }
25
+
26
+ // With result: show header + params + result
27
+ if (result) {
28
+ let outputJson =
29
+ result.content
30
+ ?.filter((c) => c.type === "text")
31
+ .map((c: any) => c.text)
32
+ .join("\n") || i18n("(no output)");
33
+ let outputLanguage = "text";
34
+
35
+ // Try to parse and pretty-print if it's valid JSON
36
+ try {
37
+ const parsed = JSON.parse(outputJson);
38
+ outputJson = JSON.stringify(parsed, null, 2);
39
+ outputLanguage = "json";
40
+ } catch {
41
+ // Not valid JSON, leave as-is and use text highlighting
42
+ }
43
+
44
+ return {
45
+ content: html`
46
+ <div class="space-y-3">
47
+ ${renderHeader(state, Code, "Tool Call")}
48
+ ${
49
+ paramsJson
50
+ ? html`<div>
51
+ <div class="text-xs font-medium mb-1 text-muted-foreground">${i18n("Input")}</div>
52
+ <code-block .code=${paramsJson} language="json"></code-block>
53
+ </div>`
54
+ : ""
55
+ }
56
+ <div>
57
+ <div class="text-xs font-medium mb-1 text-muted-foreground">${i18n("Output")}</div>
58
+ <code-block .code=${outputJson} language="${outputLanguage}"></code-block>
59
+ </div>
60
+ </div>
61
+ `,
62
+ isCustom: false,
63
+ };
64
+ }
65
+
66
+ // Just params (streaming or waiting for result)
67
+ if (params) {
68
+ if (isStreaming && (!paramsJson || paramsJson === "{}" || paramsJson === "null")) {
69
+ return {
70
+ content: html` <div>${renderHeader(state, Code, "Preparing tool parameters...")}</div> `,
71
+ isCustom: false,
72
+ };
73
+ }
74
+
75
+ return {
76
+ content: html`
77
+ <div class="space-y-3">
78
+ ${renderHeader(state, Code, "Tool Call")}
79
+ <div>
80
+ <div class="text-xs font-medium mb-1 text-muted-foreground">${i18n("Input")}</div>
81
+ <code-block .code=${paramsJson} language="json"></code-block>
82
+ </div>
83
+ </div>
84
+ `,
85
+ isCustom: false,
86
+ };
87
+ }
88
+
89
+ // No params or result yet
90
+ return {
91
+ content: html` <div>${renderHeader(state, Code, "Preparing tool...")}</div> `,
92
+ isCustom: false,
93
+ };
94
+ }
95
+ }
@@ -0,0 +1,92 @@
1
+ import type { ToolResultMessage } from "@oh-my-pi/pi-ai";
2
+ import { html } from "lit";
3
+ import { Clock } from "lucide";
4
+ import { i18n } from "../../utils/i18n.js";
5
+ import { renderHeader } from "../renderer-registry.js";
6
+ import type { ToolRenderer, ToolRenderResult } from "../types.js";
7
+
8
+ interface GetCurrentTimeParams {
9
+ timezone?: string;
10
+ }
11
+
12
+ // GetCurrentTime tool has undefined details (only uses output)
13
+ export class GetCurrentTimeRenderer implements ToolRenderer<GetCurrentTimeParams, undefined> {
14
+ render(
15
+ params: GetCurrentTimeParams | undefined,
16
+ result: ToolResultMessage<undefined> | undefined,
17
+ ): ToolRenderResult {
18
+ const state = result ? (result.isError ? "error" : "complete") : "inprogress";
19
+
20
+ // Full params + full result
21
+ if (result && params) {
22
+ const output =
23
+ result.content
24
+ ?.filter((c) => c.type === "text")
25
+ .map((c: any) => c.text)
26
+ .join("\n") || "";
27
+ const headerText = params.timezone
28
+ ? `${i18n("Getting current time in")} ${params.timezone}`
29
+ : i18n("Getting current date and time");
30
+
31
+ // Error: show header, error below
32
+ if (result.isError) {
33
+ return {
34
+ content: html`
35
+ <div class="space-y-3">
36
+ ${renderHeader(state, Clock, headerText)}
37
+ <div class="text-sm text-destructive">${output}</div>
38
+ </div>
39
+ `,
40
+ isCustom: false,
41
+ };
42
+ }
43
+
44
+ // Success: show time in header
45
+ return { content: renderHeader(state, Clock, `${headerText}: ${output}`), isCustom: false };
46
+ }
47
+
48
+ // Full result, no params
49
+ if (result) {
50
+ const output =
51
+ result.content
52
+ ?.filter((c) => c.type === "text")
53
+ .map((c: any) => c.text)
54
+ .join("\n") || "";
55
+
56
+ // Error: show header, error below
57
+ if (result.isError) {
58
+ return {
59
+ content: html`
60
+ <div class="space-y-3">
61
+ ${renderHeader(state, Clock, i18n("Getting current date and time"))}
62
+ <div class="text-sm text-destructive">${output}</div>
63
+ </div>
64
+ `,
65
+ isCustom: false,
66
+ };
67
+ }
68
+
69
+ // Success: show time in header
70
+ return {
71
+ content: renderHeader(state, Clock, `${i18n("Getting current date and time")}: ${output}`),
72
+ isCustom: false,
73
+ };
74
+ }
75
+
76
+ // Full params, no result: show timezone info in header
77
+ if (params?.timezone) {
78
+ return {
79
+ content: renderHeader(state, Clock, `${i18n("Getting current time in")} ${params.timezone}`),
80
+ isCustom: false,
81
+ };
82
+ }
83
+
84
+ // Partial params (no timezone) or empty params, no result
85
+ if (params) {
86
+ return { content: renderHeader(state, Clock, i18n("Getting current date and time")), isCustom: false };
87
+ }
88
+
89
+ // No params, no result
90
+ return { content: renderHeader(state, Clock, i18n("Getting time...")), isCustom: false };
91
+ }
92
+ }
@@ -0,0 +1,15 @@
1
+ import type { ToolResultMessage } from "@oh-my-pi/pi-ai";
2
+ import type { TemplateResult } from "lit";
3
+
4
+ export interface ToolRenderResult {
5
+ content: TemplateResult;
6
+ isCustom: boolean; // true = no card wrapper, false = wrap in card
7
+ }
8
+
9
+ export interface ToolRenderer<TParams = any, TDetails = any> {
10
+ render(
11
+ params: TParams | undefined,
12
+ result: ToolResultMessage<TDetails> | undefined,
13
+ isStreaming?: boolean,
14
+ ): ToolRenderResult;
15
+ }