@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,61 @@
1
+ # Pi Web UI - Example
2
+
3
+ This is a minimal example showing how to use `@oh-my-pi/pi-web-ui` in a web application.
4
+
5
+ ## Setup
6
+
7
+ ```bash
8
+ npm install
9
+ ```
10
+
11
+ ## Development
12
+
13
+ ```bash
14
+ npm run dev
15
+ ```
16
+
17
+ Open [http://localhost:5173](http://localhost:5173) in your browser.
18
+
19
+ ## What's Included
20
+
21
+ This example demonstrates:
22
+
23
+ - **ChatPanel** - The main chat interface component
24
+ - **System Prompt** - Custom configuration for the AI assistant
25
+ - **Tools** - JavaScript REPL and artifacts tool
26
+
27
+ ## Configuration
28
+
29
+ ### API Keys
30
+
31
+ The example uses **Direct Mode** by default, which means it calls AI provider APIs directly from the browser.
32
+
33
+ To use the chat:
34
+
35
+ 1. Click the settings icon (⚙️) in the chat interface
36
+ 2. Click "Manage API Keys"
37
+ 3. Add your API key for your preferred provider:
38
+ - **Anthropic**: Get a key from [console.anthropic.com](https://console.anthropic.com/)
39
+ - **OpenAI**: Get a key from [platform.openai.com](https://platform.openai.com/)
40
+ - **Google**: Get a key from [makersuite.google.com](https://makersuite.google.com/)
41
+
42
+ API keys are stored in your browser's localStorage and never sent to any server except the AI provider's API.
43
+
44
+ ## Project Structure
45
+
46
+ ```
47
+ example/
48
+ ├── src/
49
+ │ ├── main.ts # Main application entry point
50
+ │ └── app.css # Tailwind CSS configuration
51
+ ├── index.html # HTML entry point
52
+ ├── package.json # Dependencies
53
+ ├── vite.config.ts # Vite configuration
54
+ └── tsconfig.json # TypeScript configuration
55
+ ```
56
+
57
+ ## Learn More
58
+
59
+ - [Pi Web UI Documentation](../README.md)
60
+ - [Pi AI Documentation](../../ai/README.md)
61
+ - [Mini Lit Documentation](https://github.com/badlogic/mini-lit)
@@ -0,0 +1,13 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>Pi Web UI - Example</title>
7
+ <meta name="description" content="Example usage of @oh-my-pi/pi-web-ui - Reusable AI chat interface" />
8
+ </head>
9
+ <body class="bg-background">
10
+ <div id="app"></div>
11
+ <script type="module" src="/src/main.ts"></script>
12
+ </body>
13
+ </html>
@@ -0,0 +1,24 @@
1
+ {
2
+ "name": "pi-web-ui-example",
3
+ "version": "1.19.1",
4
+ "private": true,
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "vite",
8
+ "build": "vite build",
9
+ "preview": "vite preview",
10
+ "clean": "rm -rf dist",
11
+ "check": "biome check --write . && tsgo --noEmit"
12
+ },
13
+ "dependencies": {
14
+ "@mariozechner/mini-lit": "^0.2.0",
15
+ "@oh-my-pi/pi-ai": "workspace:*",
16
+ "@oh-my-pi/pi-web-ui": "workspace:*",
17
+ "@tailwindcss/vite": "^4.1.17",
18
+ "lit": "^3.3.1",
19
+ "lucide": "^0.544.0"
20
+ },
21
+ "devDependencies": {
22
+ "vite": "^7.1.6"
23
+ }
24
+ }
@@ -0,0 +1 @@
1
+ @import "../../dist/app.css";
@@ -0,0 +1,99 @@
1
+ import { Alert } from "@mariozechner/mini-lit/dist/Alert.js";
2
+ import type { Message } from "@oh-my-pi/pi-ai";
3
+ import type { AgentMessage, MessageRenderer } from "@oh-my-pi/pi-web-ui";
4
+ import { defaultConvertToLlm, registerMessageRenderer } from "@oh-my-pi/pi-web-ui";
5
+ import { html } from "lit";
6
+
7
+ // ============================================================================
8
+ // 1. EXTEND AppMessage TYPE VIA DECLARATION MERGING
9
+ // ============================================================================
10
+
11
+ // Define custom message types
12
+ export interface SystemNotificationMessage {
13
+ role: "system-notification";
14
+ message: string;
15
+ variant: "default" | "destructive";
16
+ timestamp: string;
17
+ }
18
+
19
+ // Extend CustomAgentMessages interface via declaration merging
20
+ // This must target pi-agent-core where CustomAgentMessages is defined
21
+ declare module "@oh-my-pi/pi-agent-core" {
22
+ interface CustomAgentMessages {
23
+ "system-notification": SystemNotificationMessage;
24
+ }
25
+ }
26
+
27
+ // ============================================================================
28
+ // 2. CREATE CUSTOM RENDERER (TYPED TO SystemNotificationMessage)
29
+ // ============================================================================
30
+
31
+ const systemNotificationRenderer: MessageRenderer<SystemNotificationMessage> = {
32
+ render: (notification) => {
33
+ // notification is fully typed as SystemNotificationMessage!
34
+ return html`
35
+ <div class="px-4">
36
+ ${Alert({
37
+ variant: notification.variant,
38
+ children: html`
39
+ <div class="flex flex-col gap-1">
40
+ <div>${notification.message}</div>
41
+ <div class="text-xs opacity-70">${new Date(notification.timestamp).toLocaleTimeString()}</div>
42
+ </div>
43
+ `,
44
+ })}
45
+ </div>
46
+ `;
47
+ },
48
+ };
49
+
50
+ // ============================================================================
51
+ // 3. REGISTER RENDERER
52
+ // ============================================================================
53
+
54
+ export function registerCustomMessageRenderers() {
55
+ registerMessageRenderer("system-notification", systemNotificationRenderer);
56
+ }
57
+
58
+ // ============================================================================
59
+ // 4. HELPER TO CREATE CUSTOM MESSAGES
60
+ // ============================================================================
61
+
62
+ export function createSystemNotification(
63
+ message: string,
64
+ variant: "default" | "destructive" = "default",
65
+ ): SystemNotificationMessage {
66
+ return {
67
+ role: "system-notification",
68
+ message,
69
+ variant,
70
+ timestamp: new Date().toISOString(),
71
+ };
72
+ }
73
+
74
+ // ============================================================================
75
+ // 5. CUSTOM MESSAGE TRANSFORMER
76
+ // ============================================================================
77
+
78
+ /**
79
+ * Custom message transformer that extends defaultConvertToLlm.
80
+ * Handles system-notification messages by converting them to user messages.
81
+ */
82
+ export function customConvertToLlm(messages: AgentMessage[]): Message[] {
83
+ // First, handle our custom system-notification type
84
+ const processed = messages.map((m): AgentMessage => {
85
+ if (m.role === "system-notification") {
86
+ const notification = m as SystemNotificationMessage;
87
+ // Convert to user message with <system> tags
88
+ return {
89
+ role: "user",
90
+ content: `<system>${notification.message}</system>`,
91
+ timestamp: Date.now(),
92
+ };
93
+ }
94
+ return m;
95
+ });
96
+
97
+ // Then use defaultConvertToLlm for standard handling
98
+ return defaultConvertToLlm(processed);
99
+ }
@@ -0,0 +1,420 @@
1
+ import "@mariozechner/mini-lit/dist/ThemeToggle.js";
2
+ import { Agent, type AgentMessage } from "@oh-my-pi/pi-agent-core";
3
+ import { getModel } from "@oh-my-pi/pi-ai";
4
+ import {
5
+ type AgentState,
6
+ ApiKeyPromptDialog,
7
+ AppStorage,
8
+ ChatPanel,
9
+ CustomProvidersStore,
10
+ createJavaScriptReplTool,
11
+ IndexedDBStorageBackend,
12
+ // PersistentStorageDialog, // TODO: Fix - currently broken
13
+ ProviderKeysStore,
14
+ ProvidersModelsTab,
15
+ ProxyTab,
16
+ SessionListDialog,
17
+ SessionsStore,
18
+ SettingsDialog,
19
+ SettingsStore,
20
+ setAppStorage,
21
+ } from "@oh-my-pi/pi-web-ui";
22
+ import { html, render } from "lit";
23
+ import { Bell, History, Plus, Settings } from "lucide";
24
+ import "./app.css";
25
+ import { icon } from "@mariozechner/mini-lit";
26
+ import { Button } from "@mariozechner/mini-lit/dist/Button.js";
27
+ import { Input } from "@mariozechner/mini-lit/dist/Input.js";
28
+ import { createSystemNotification, customConvertToLlm, registerCustomMessageRenderers } from "./custom-messages.js";
29
+
30
+ // Register custom message renderers
31
+ registerCustomMessageRenderers();
32
+
33
+ // Create stores
34
+ const settings = new SettingsStore();
35
+ const providerKeys = new ProviderKeysStore();
36
+ const sessions = new SessionsStore();
37
+ const customProviders = new CustomProvidersStore();
38
+
39
+ // Gather configs
40
+ const configs = [
41
+ settings.getConfig(),
42
+ SessionsStore.getMetadataConfig(),
43
+ providerKeys.getConfig(),
44
+ customProviders.getConfig(),
45
+ sessions.getConfig(),
46
+ ];
47
+
48
+ // Create backend
49
+ const backend = new IndexedDBStorageBackend({
50
+ dbName: "pi-web-ui-example",
51
+ version: 2, // Incremented for custom-providers store
52
+ stores: configs,
53
+ });
54
+
55
+ // Wire backend to stores
56
+ settings.setBackend(backend);
57
+ providerKeys.setBackend(backend);
58
+ customProviders.setBackend(backend);
59
+ sessions.setBackend(backend);
60
+
61
+ // Create and set app storage
62
+ const storage = new AppStorage(settings, providerKeys, sessions, customProviders, backend);
63
+ setAppStorage(storage);
64
+
65
+ let currentSessionId: string | undefined;
66
+ let currentTitle = "";
67
+ let isEditingTitle = false;
68
+ let agent: Agent;
69
+ let chatPanel: ChatPanel;
70
+ let agentUnsubscribe: (() => void) | undefined;
71
+
72
+ const generateTitle = (messages: AgentMessage[]): string => {
73
+ const firstUserMsg = messages.find((m) => m.role === "user" || m.role === "user-with-attachments");
74
+ if (!firstUserMsg || (firstUserMsg.role !== "user" && firstUserMsg.role !== "user-with-attachments")) return "";
75
+
76
+ let text = "";
77
+ const content = firstUserMsg.content;
78
+
79
+ if (typeof content === "string") {
80
+ text = content;
81
+ } else {
82
+ const textBlocks = content.filter((c: any) => c.type === "text");
83
+ text = textBlocks.map((c: any) => c.text || "").join(" ");
84
+ }
85
+
86
+ text = text.trim();
87
+ if (!text) return "";
88
+
89
+ const sentenceEnd = text.search(/[.!?]/);
90
+ if (sentenceEnd > 0 && sentenceEnd <= 50) {
91
+ return text.substring(0, sentenceEnd + 1);
92
+ }
93
+ return text.length <= 50 ? text : `${text.substring(0, 47)}...`;
94
+ };
95
+
96
+ const shouldSaveSession = (messages: AgentMessage[]): boolean => {
97
+ const hasUserMsg = messages.some((m: any) => m.role === "user" || m.role === "user-with-attachments");
98
+ const hasAssistantMsg = messages.some((m: any) => m.role === "assistant");
99
+ return hasUserMsg && hasAssistantMsg;
100
+ };
101
+
102
+ const saveSession = async () => {
103
+ if (!storage.sessions || !currentSessionId || !agent || !currentTitle) return;
104
+
105
+ const state = agent.state;
106
+ if (!shouldSaveSession(state.messages)) return;
107
+
108
+ try {
109
+ // Create session data
110
+ const sessionData = {
111
+ id: currentSessionId,
112
+ title: currentTitle,
113
+ model: state.model!,
114
+ thinkingLevel: state.thinkingLevel,
115
+ messages: state.messages,
116
+ createdAt: new Date().toISOString(),
117
+ lastModified: new Date().toISOString(),
118
+ };
119
+
120
+ // Create session metadata
121
+ const metadata = {
122
+ id: currentSessionId,
123
+ title: currentTitle,
124
+ createdAt: sessionData.createdAt,
125
+ lastModified: sessionData.lastModified,
126
+ messageCount: state.messages.length,
127
+ usage: {
128
+ input: 0,
129
+ output: 0,
130
+ cacheRead: 0,
131
+ cacheWrite: 0,
132
+ totalTokens: 0,
133
+ cost: {
134
+ input: 0,
135
+ output: 0,
136
+ cacheRead: 0,
137
+ cacheWrite: 0,
138
+ total: 0,
139
+ },
140
+ },
141
+ modelId: state.model?.id || null,
142
+ thinkingLevel: state.thinkingLevel,
143
+ preview: generateTitle(state.messages),
144
+ };
145
+
146
+ await storage.sessions.save(sessionData, metadata);
147
+ } catch (err) {
148
+ console.error("Failed to save session:", err);
149
+ }
150
+ };
151
+
152
+ const updateUrl = (sessionId: string) => {
153
+ const url = new URL(window.location.href);
154
+ url.searchParams.set("session", sessionId);
155
+ window.history.replaceState({}, "", url);
156
+ };
157
+
158
+ const createAgent = async (initialState?: Partial<AgentState>) => {
159
+ if (agentUnsubscribe) {
160
+ agentUnsubscribe();
161
+ }
162
+
163
+ agent = new Agent({
164
+ initialState: initialState || {
165
+ systemPrompt: `You are a helpful AI assistant with access to various tools.
166
+
167
+ Available tools:
168
+ - JavaScript REPL: Execute JavaScript code in a sandboxed browser environment (can do calculations, get time, process data, create visualizations, etc.)
169
+ - Artifacts: Create interactive HTML, SVG, Markdown, and text artifacts
170
+
171
+ Feel free to use these tools when needed to provide accurate and helpful responses.`,
172
+ model: getModel("anthropic", "claude-sonnet-4-5-20250929"),
173
+ thinkingLevel: "off",
174
+ messages: [],
175
+ tools: [],
176
+ },
177
+ // Custom transformer: convert custom messages to LLM-compatible format
178
+ convertToLlm: customConvertToLlm,
179
+ });
180
+
181
+ agentUnsubscribe = agent.subscribe((event: any) => {
182
+ if (event.type === "state-update") {
183
+ const messages = event.state.messages;
184
+
185
+ // Generate title after first successful response
186
+ if (!currentTitle && shouldSaveSession(messages)) {
187
+ currentTitle = generateTitle(messages);
188
+ }
189
+
190
+ // Create session ID on first successful save
191
+ if (!currentSessionId && shouldSaveSession(messages)) {
192
+ currentSessionId = crypto.randomUUID();
193
+ updateUrl(currentSessionId);
194
+ }
195
+
196
+ // Auto-save
197
+ if (currentSessionId) {
198
+ saveSession();
199
+ }
200
+
201
+ renderApp();
202
+ }
203
+ });
204
+
205
+ await chatPanel.setAgent(agent, {
206
+ onApiKeyRequired: async (provider: string) => {
207
+ return await ApiKeyPromptDialog.prompt(provider);
208
+ },
209
+ toolsFactory: (_agent, _agentInterface, _artifactsPanel, runtimeProvidersFactory) => {
210
+ // Create javascript_repl tool with access to attachments + artifacts
211
+ const replTool = createJavaScriptReplTool();
212
+ replTool.runtimeProvidersFactory = runtimeProvidersFactory;
213
+ return [replTool];
214
+ },
215
+ });
216
+ };
217
+
218
+ const loadSession = async (sessionId: string): Promise<boolean> => {
219
+ if (!storage.sessions) return false;
220
+
221
+ const sessionData = await storage.sessions.get(sessionId);
222
+ if (!sessionData) {
223
+ console.error("Session not found:", sessionId);
224
+ return false;
225
+ }
226
+
227
+ currentSessionId = sessionId;
228
+ const metadata = await storage.sessions.getMetadata(sessionId);
229
+ currentTitle = metadata?.title || "";
230
+
231
+ await createAgent({
232
+ model: sessionData.model,
233
+ thinkingLevel: sessionData.thinkingLevel,
234
+ messages: sessionData.messages,
235
+ tools: [],
236
+ });
237
+
238
+ updateUrl(sessionId);
239
+ renderApp();
240
+ return true;
241
+ };
242
+
243
+ const newSession = () => {
244
+ const url = new URL(window.location.href);
245
+ url.search = "";
246
+ window.location.href = url.toString();
247
+ };
248
+
249
+ // ============================================================================
250
+ // RENDER
251
+ // ============================================================================
252
+ const renderApp = () => {
253
+ const app = document.getElementById("app");
254
+ if (!app) return;
255
+
256
+ const appHtml = html`
257
+ <div class="w-full h-screen flex flex-col bg-background text-foreground overflow-hidden">
258
+ <!-- Header -->
259
+ <div class="flex items-center justify-between border-b border-border shrink-0">
260
+ <div class="flex items-center gap-2 px-4 py-">
261
+ ${Button({
262
+ variant: "ghost",
263
+ size: "sm",
264
+ children: icon(History, "sm"),
265
+ onClick: () => {
266
+ SessionListDialog.open(
267
+ async (sessionId) => {
268
+ await loadSession(sessionId);
269
+ },
270
+ (deletedSessionId) => {
271
+ // Only reload if the current session was deleted
272
+ if (deletedSessionId === currentSessionId) {
273
+ newSession();
274
+ }
275
+ },
276
+ );
277
+ },
278
+ title: "Sessions",
279
+ })}
280
+ ${Button({
281
+ variant: "ghost",
282
+ size: "sm",
283
+ children: icon(Plus, "sm"),
284
+ onClick: newSession,
285
+ title: "New Session",
286
+ })}
287
+ ${
288
+ currentTitle
289
+ ? isEditingTitle
290
+ ? html`<div class="flex items-center gap-2">
291
+ ${Input({
292
+ type: "text",
293
+ value: currentTitle,
294
+ className: "text-sm w-64",
295
+ onChange: async (e: Event) => {
296
+ const newTitle = (e.target as HTMLInputElement).value.trim();
297
+ if (newTitle && newTitle !== currentTitle && storage.sessions && currentSessionId) {
298
+ await storage.sessions.updateTitle(currentSessionId, newTitle);
299
+ currentTitle = newTitle;
300
+ }
301
+ isEditingTitle = false;
302
+ renderApp();
303
+ },
304
+ onKeyDown: async (e: KeyboardEvent) => {
305
+ if (e.key === "Enter") {
306
+ const newTitle = (e.target as HTMLInputElement).value.trim();
307
+ if (newTitle && newTitle !== currentTitle && storage.sessions && currentSessionId) {
308
+ await storage.sessions.updateTitle(currentSessionId, newTitle);
309
+ currentTitle = newTitle;
310
+ }
311
+ isEditingTitle = false;
312
+ renderApp();
313
+ } else if (e.key === "Escape") {
314
+ isEditingTitle = false;
315
+ renderApp();
316
+ }
317
+ },
318
+ })}
319
+ </div>`
320
+ : html`<button
321
+ class="px-2 py-1 text-sm text-foreground hover:bg-secondary rounded transition-colors"
322
+ @click=${() => {
323
+ isEditingTitle = true;
324
+ renderApp();
325
+ requestAnimationFrame(() => {
326
+ const input = app?.querySelector('input[type="text"]') as HTMLInputElement;
327
+ if (input) {
328
+ input.focus();
329
+ input.select();
330
+ }
331
+ });
332
+ }}
333
+ title="Click to edit title"
334
+ >
335
+ ${currentTitle}
336
+ </button>`
337
+ : html`<span class="text-base font-semibold text-foreground">Pi Web UI Example</span>`
338
+ }
339
+ </div>
340
+ <div class="flex items-center gap-1 px-2">
341
+ ${Button({
342
+ variant: "ghost",
343
+ size: "sm",
344
+ children: icon(Bell, "sm"),
345
+ onClick: () => {
346
+ // Demo: Inject custom message (will appear on next agent run)
347
+ if (agent) {
348
+ agent.queueMessage(
349
+ createSystemNotification(
350
+ "This is a custom message! It appears in the UI but is never sent to the LLM.",
351
+ ),
352
+ );
353
+ }
354
+ },
355
+ title: "Demo: Add Custom Notification",
356
+ })}
357
+ <theme-toggle></theme-toggle>
358
+ ${Button({
359
+ variant: "ghost",
360
+ size: "sm",
361
+ children: icon(Settings, "sm"),
362
+ onClick: () => SettingsDialog.open([new ProvidersModelsTab(), new ProxyTab()]),
363
+ title: "Settings",
364
+ })}
365
+ </div>
366
+ </div>
367
+
368
+ <!-- Chat Panel -->
369
+ ${chatPanel}
370
+ </div>
371
+ `;
372
+
373
+ render(appHtml, app);
374
+ };
375
+
376
+ // ============================================================================
377
+ // INIT
378
+ // ============================================================================
379
+ async function initApp() {
380
+ const app = document.getElementById("app");
381
+ if (!app) throw new Error("App container not found");
382
+
383
+ // Show loading
384
+ render(
385
+ html`
386
+ <div class="w-full h-screen flex items-center justify-center bg-background text-foreground">
387
+ <div class="text-muted-foreground">Loading...</div>
388
+ </div>
389
+ `,
390
+ app,
391
+ );
392
+
393
+ // TODO: Fix PersistentStorageDialog - currently broken
394
+ // Request persistent storage
395
+ // if (storage.sessions) {
396
+ // await PersistentStorageDialog.request();
397
+ // }
398
+
399
+ // Create ChatPanel
400
+ chatPanel = new ChatPanel();
401
+
402
+ // Check for session in URL
403
+ const urlParams = new URLSearchParams(window.location.search);
404
+ const sessionIdFromUrl = urlParams.get("session");
405
+
406
+ if (sessionIdFromUrl) {
407
+ const loaded = await loadSession(sessionIdFromUrl);
408
+ if (!loaded) {
409
+ // Session doesn't exist, redirect to new session
410
+ newSession();
411
+ return;
412
+ }
413
+ } else {
414
+ await createAgent();
415
+ }
416
+
417
+ renderApp();
418
+ }
419
+
420
+ initApp();
@@ -0,0 +1,23 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "ES2022",
5
+ "lib": ["ES2022", "DOM", "DOM.Iterable"],
6
+ "moduleResolution": "bundler",
7
+ "paths": {
8
+ "*": ["./*"],
9
+ "@oh-my-pi/pi-agent-core": ["../../agent/dist/index.d.ts"],
10
+ "@oh-my-pi/pi-ai": ["../../ai/dist/index.d.ts"],
11
+ "@oh-my-pi/pi-tui": ["../../tui/dist/index.d.ts"],
12
+ "@oh-my-pi/pi-web-ui": ["../dist/index.d.ts"]
13
+ },
14
+ "strict": true,
15
+ "skipLibCheck": true,
16
+ "esModuleInterop": true,
17
+ "allowSyntheticDefaultImports": true,
18
+ "experimentalDecorators": true,
19
+ "useDefineForClassFields": false
20
+ },
21
+ "include": ["src/**/*"],
22
+ "exclude": ["../src"]
23
+ }
@@ -0,0 +1,6 @@
1
+ import tailwindcss from "@tailwindcss/vite";
2
+ import { defineConfig } from "vite";
3
+
4
+ export default defineConfig({
5
+ plugins: [tailwindcss()],
6
+ });