@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.
- package/CHANGELOG.md +96 -0
- package/README.md +609 -0
- package/example/README.md +61 -0
- package/example/index.html +13 -0
- package/example/package.json +24 -0
- package/example/src/app.css +1 -0
- package/example/src/custom-messages.ts +99 -0
- package/example/src/main.ts +420 -0
- package/example/tsconfig.json +23 -0
- package/example/vite.config.ts +6 -0
- package/package.json +57 -0
- package/scripts/count-prompt-tokens.ts +88 -0
- package/src/ChatPanel.ts +218 -0
- package/src/app.css +68 -0
- package/src/components/AgentInterface.ts +390 -0
- package/src/components/AttachmentTile.ts +107 -0
- package/src/components/ConsoleBlock.ts +74 -0
- package/src/components/CustomProviderCard.ts +96 -0
- package/src/components/ExpandableSection.ts +46 -0
- package/src/components/Input.ts +113 -0
- package/src/components/MessageEditor.ts +404 -0
- package/src/components/MessageList.ts +97 -0
- package/src/components/Messages.ts +384 -0
- package/src/components/ProviderKeyInput.ts +152 -0
- package/src/components/SandboxedIframe.ts +626 -0
- package/src/components/StreamingMessageContainer.ts +107 -0
- package/src/components/ThinkingBlock.ts +45 -0
- package/src/components/message-renderer-registry.ts +28 -0
- package/src/components/sandbox/ArtifactsRuntimeProvider.ts +219 -0
- package/src/components/sandbox/AttachmentsRuntimeProvider.ts +66 -0
- package/src/components/sandbox/ConsoleRuntimeProvider.ts +186 -0
- package/src/components/sandbox/FileDownloadRuntimeProvider.ts +110 -0
- package/src/components/sandbox/RuntimeMessageBridge.ts +82 -0
- package/src/components/sandbox/RuntimeMessageRouter.ts +216 -0
- package/src/components/sandbox/SandboxRuntimeProvider.ts +52 -0
- package/src/dialogs/ApiKeyPromptDialog.ts +75 -0
- package/src/dialogs/AttachmentOverlay.ts +640 -0
- package/src/dialogs/CustomProviderDialog.ts +274 -0
- package/src/dialogs/ModelSelector.ts +314 -0
- package/src/dialogs/PersistentStorageDialog.ts +146 -0
- package/src/dialogs/ProvidersModelsTab.ts +212 -0
- package/src/dialogs/SessionListDialog.ts +157 -0
- package/src/dialogs/SettingsDialog.ts +216 -0
- package/src/index.ts +115 -0
- package/src/prompts/prompts.ts +282 -0
- package/src/storage/app-storage.ts +60 -0
- package/src/storage/backends/indexeddb-storage-backend.ts +193 -0
- package/src/storage/store.ts +33 -0
- package/src/storage/stores/custom-providers-store.ts +62 -0
- package/src/storage/stores/provider-keys-store.ts +33 -0
- package/src/storage/stores/sessions-store.ts +136 -0
- package/src/storage/stores/settings-store.ts +34 -0
- package/src/storage/types.ts +206 -0
- package/src/tools/artifacts/ArtifactElement.ts +14 -0
- package/src/tools/artifacts/ArtifactPill.ts +26 -0
- package/src/tools/artifacts/Console.ts +102 -0
- package/src/tools/artifacts/DocxArtifact.ts +213 -0
- package/src/tools/artifacts/ExcelArtifact.ts +231 -0
- package/src/tools/artifacts/GenericArtifact.ts +118 -0
- package/src/tools/artifacts/HtmlArtifact.ts +203 -0
- package/src/tools/artifacts/ImageArtifact.ts +116 -0
- package/src/tools/artifacts/MarkdownArtifact.ts +83 -0
- package/src/tools/artifacts/PdfArtifact.ts +201 -0
- package/src/tools/artifacts/SvgArtifact.ts +82 -0
- package/src/tools/artifacts/TextArtifact.ts +148 -0
- package/src/tools/artifacts/artifacts-tool-renderer.ts +371 -0
- package/src/tools/artifacts/artifacts.ts +713 -0
- package/src/tools/artifacts/index.ts +7 -0
- package/src/tools/extract-document.ts +271 -0
- package/src/tools/index.ts +46 -0
- package/src/tools/javascript-repl.ts +316 -0
- package/src/tools/renderer-registry.ts +127 -0
- package/src/tools/renderers/BashRenderer.ts +52 -0
- package/src/tools/renderers/CalculateRenderer.ts +58 -0
- package/src/tools/renderers/DefaultRenderer.ts +95 -0
- package/src/tools/renderers/GetCurrentTimeRenderer.ts +92 -0
- package/src/tools/types.ts +15 -0
- package/src/utils/attachment-utils.ts +472 -0
- package/src/utils/auth-token.ts +22 -0
- package/src/utils/format.ts +42 -0
- package/src/utils/i18n.ts +653 -0
- package/src/utils/model-discovery.ts +277 -0
- package/src/utils/proxy-utils.ts +134 -0
- package/src/utils/test-sessions.ts +2357 -0
- package/tsconfig.build.json +20 -0
- 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
|
+
}
|