
Security News
pnpm 11.5 Adds Support for Recognizing npm Staged Publishes
pnpm 11.5 now recognizes npm staged publish approvals in release metadata, preventing those releases from being mistaken for lower-trust package publishes.
@mariozechner/pi-web-ui
Advanced tools
Reusable web UI components for AI chat interfaces powered by @mariozechner/pi-ai
Reusable web UI components for building AI chat interfaces powered by @mariozechner/pi-ai and @mariozechner/pi-agent-core.
Built with mini-lit web components and Tailwind CSS v4.
npm install @mariozechner/pi-web-ui @mariozechner/pi-agent-core @mariozechner/pi-ai
See the example directory for a complete working application.
import { Agent } from '@mariozechner/pi-agent-core';
import { getModel } from '@mariozechner/pi-ai';
import {
ChatPanel,
AppStorage,
IndexedDBStorageBackend,
ProviderKeysStore,
SessionsStore,
SettingsStore,
setAppStorage,
defaultConvertToLlm,
ApiKeyPromptDialog,
} from '@mariozechner/pi-web-ui';
import '@mariozechner/pi-web-ui/app.css';
// Set up storage
const settings = new SettingsStore();
const providerKeys = new ProviderKeysStore();
const sessions = new SessionsStore();
const backend = new IndexedDBStorageBackend({
dbName: 'my-app',
version: 1,
stores: [
settings.getConfig(),
providerKeys.getConfig(),
sessions.getConfig(),
SessionsStore.getMetadataConfig(),
],
});
settings.setBackend(backend);
providerKeys.setBackend(backend);
sessions.setBackend(backend);
const storage = new AppStorage(settings, providerKeys, sessions, undefined, backend);
setAppStorage(storage);
// Create agent
const agent = new Agent({
initialState: {
systemPrompt: 'You are a helpful assistant.',
model: getModel('anthropic', 'claude-sonnet-4-5-20250929'),
thinkingLevel: 'off',
messages: [],
tools: [],
},
convertToLlm: defaultConvertToLlm,
});
// Create chat panel
const chatPanel = new ChatPanel();
await chatPanel.setAgent(agent, {
onApiKeyRequired: (provider) => ApiKeyPromptDialog.prompt(provider),
});
document.body.appendChild(chatPanel);
┌─────────────────────────────────────────────────────┐
│ ChatPanel │
│ ┌─────────────────────┐ ┌─────────────────────┐ │
│ │ AgentInterface │ │ ArtifactsPanel │ │
│ │ (messages, input) │ │ (HTML, SVG, MD) │ │
│ └─────────────────────┘ └─────────────────────┘ │
└─────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────┐
│ Agent (from pi-agent-core) │
│ - State management (messages, model, tools) │
│ - Event emission (agent_start, message_update, ...)│
│ - Tool execution │
└─────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────┐
│ AppStorage │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ Settings │ │ Provider │ │ Sessions │ │
│ │ Store │ │Keys Store│ │ Store │ │
│ └──────────┘ └──────────┘ └──────────┘ │
│ │ │
│ IndexedDBStorageBackend │
└─────────────────────────────────────────────────────┘
High-level chat interface with built-in artifacts panel.
const chatPanel = new ChatPanel();
await chatPanel.setAgent(agent, {
// Prompt for API key when needed
onApiKeyRequired: async (provider) => ApiKeyPromptDialog.prompt(provider),
// Hook before sending messages
onBeforeSend: async () => { /* save draft, etc. */ },
// Handle cost display click
onCostClick: () => { /* show cost breakdown */ },
// Custom sandbox URL for browser extensions
sandboxUrlProvider: () => chrome.runtime.getURL('sandbox.html'),
// Add custom tools
toolsFactory: (agent, agentInterface, artifactsPanel, runtimeProvidersFactory) => {
const replTool = createJavaScriptReplTool();
replTool.runtimeProvidersFactory = runtimeProvidersFactory;
return [replTool];
},
});
Lower-level chat interface for custom layouts.
const chat = document.createElement('agent-interface') as AgentInterface;
chat.session = agent;
chat.enableAttachments = true;
chat.enableModelSelector = true;
chat.enableThinkingSelector = true;
chat.onApiKeyRequired = async (provider) => { /* ... */ };
chat.onBeforeSend = async () => { /* ... */ };
Properties:
session: Agent instanceenableAttachments: Show attachment button (default: true)enableModelSelector: Show model selector (default: true)enableThinkingSelector: Show thinking level selector (default: true)showThemeToggle: Show theme toggle (default: false)import { Agent } from '@mariozechner/pi-agent-core';
const agent = new Agent({
initialState: {
model: getModel('anthropic', 'claude-sonnet-4-5-20250929'),
systemPrompt: 'You are helpful.',
thinkingLevel: 'off',
messages: [],
tools: [],
},
convertToLlm: defaultConvertToLlm,
});
// Events
agent.subscribe((event) => {
switch (event.type) {
case 'agent_start': // Agent loop started
case 'agent_end': // Agent loop finished
case 'turn_start': // LLM call started
case 'turn_end': // LLM call finished
case 'message_start':
case 'message_update': // Streaming update
case 'message_end':
break;
}
});
// Send message
await agent.prompt('Hello!');
await agent.prompt({ role: 'user-with-attachments', content: 'Check this', attachments, timestamp: Date.now() });
// Control
agent.abort();
agent.state.model = newModel;
agent.state.thinkingLevel = 'medium';
agent.state.tools = [...];
agent.followUp(customMessage);
User message with file attachments:
const message: UserMessageWithAttachments = {
role: 'user-with-attachments',
content: 'Analyze this document',
attachments: [pdfAttachment],
timestamp: Date.now(),
};
// Type guard
if (isUserMessageWithAttachments(msg)) {
console.log(msg.attachments);
}
For session persistence of artifacts:
const artifact: ArtifactMessage = {
role: 'artifact',
action: 'create', // or 'update', 'delete'
filename: 'chart.html',
content: '<div>...</div>',
timestamp: new Date().toISOString(),
};
// Type guard
if (isArtifactMessage(msg)) {
console.log(msg.filename);
}
Extend via declaration merging:
interface SystemNotification {
role: 'system-notification';
message: string;
level: 'info' | 'warning' | 'error';
timestamp: string;
}
declare module '@mariozechner/pi-agent-core' {
interface CustomAgentMessages {
'system-notification': SystemNotification;
}
}
// Register renderer
registerMessageRenderer('system-notification', {
render: (msg) => html`<div class="alert">${msg.message}</div>`,
});
// Extend convertToLlm
function myConvertToLlm(messages: AgentMessage[]): Message[] {
const processed = messages.map((m) => {
if (m.role === 'system-notification') {
return { role: 'user', content: `<system>${m.message}</system>`, timestamp: Date.now() };
}
return m;
});
return defaultConvertToLlm(processed);
}
convertToLlm transforms app messages to LLM-compatible format:
import { defaultConvertToLlm, convertAttachments } from '@mariozechner/pi-web-ui';
// defaultConvertToLlm handles:
// - UserMessageWithAttachments → user message with image/text content blocks
// - ArtifactMessage → filtered out (UI-only)
// - Standard messages (user, assistant, toolResult) → passed through
Execute JavaScript in a sandboxed browser environment:
import { createJavaScriptReplTool } from '@mariozechner/pi-web-ui';
const replTool = createJavaScriptReplTool();
// Configure runtime providers for artifact/attachment access
replTool.runtimeProvidersFactory = () => [
new AttachmentsRuntimeProvider(attachments),
new ArtifactsRuntimeProvider(artifactsPanel, agent, true), // read-write
];
agent.state.tools = [replTool];
Extract text from documents at URLs:
import { createExtractDocumentTool } from '@mariozechner/pi-web-ui';
const extractTool = createExtractDocumentTool();
extractTool.corsProxyUrl = 'https://corsproxy.io/?';
agent.state.tools = [extractTool];
Built into ArtifactsPanel, supports: HTML, SVG, Markdown, text, JSON, images, PDF, DOCX, XLSX.
const artifactsPanel = new ArtifactsPanel();
artifactsPanel.agent = agent;
// The tool is available as artifactsPanel.tool
agent.state.tools = [artifactsPanel.tool];
import { registerToolRenderer, type ToolRenderer } from '@mariozechner/pi-web-ui';
const myRenderer: ToolRenderer = {
render(params, result, isStreaming) {
return {
content: html`<div>...</div>`,
isCustom: false, // true = no card wrapper
};
},
};
registerToolRenderer('my_tool', myRenderer);
import {
AppStorage,
IndexedDBStorageBackend,
SettingsStore,
ProviderKeysStore,
SessionsStore,
CustomProvidersStore,
setAppStorage,
getAppStorage,
} from '@mariozechner/pi-web-ui';
// Create stores
const settings = new SettingsStore();
const providerKeys = new ProviderKeysStore();
const sessions = new SessionsStore();
const customProviders = new CustomProvidersStore();
// Create backend with all store configs
const backend = new IndexedDBStorageBackend({
dbName: 'my-app',
version: 1,
stores: [
settings.getConfig(),
providerKeys.getConfig(),
sessions.getConfig(),
SessionsStore.getMetadataConfig(),
customProviders.getConfig(),
],
});
// Wire stores to backend
settings.setBackend(backend);
providerKeys.setBackend(backend);
sessions.setBackend(backend);
customProviders.setBackend(backend);
// Create and set global storage
const storage = new AppStorage(settings, providerKeys, sessions, customProviders, backend);
setAppStorage(storage);
Key-value settings:
await storage.settings.set('proxy.enabled', true);
await storage.settings.set('proxy.url', 'https://proxy.example.com');
const enabled = await storage.settings.get<boolean>('proxy.enabled');
API keys by provider:
await storage.providerKeys.set('anthropic', 'sk-ant-...');
const key = await storage.providerKeys.get('anthropic');
const providers = await storage.providerKeys.list();
Chat sessions with metadata:
// Save session
await storage.sessions.save(sessionData, metadata);
// Load session
const data = await storage.sessions.get(sessionId);
const metadata = await storage.sessions.getMetadata(sessionId);
// List sessions (sorted by lastModified)
const allMetadata = await storage.sessions.getAllMetadata();
// Update title
await storage.sessions.updateTitle(sessionId, 'New Title');
// Delete
await storage.sessions.delete(sessionId);
Custom LLM providers:
const provider: CustomProvider = {
id: crypto.randomUUID(),
name: 'My Ollama',
type: 'ollama',
baseUrl: 'http://localhost:11434',
};
await storage.customProviders.set(provider);
const all = await storage.customProviders.getAll();
Load and process files:
import { loadAttachment, type Attachment } from '@mariozechner/pi-web-ui';
// From File input
const file = inputElement.files[0];
const attachment = await loadAttachment(file);
// From URL
const attachment = await loadAttachment('https://example.com/doc.pdf');
// From ArrayBuffer
const attachment = await loadAttachment(arrayBuffer, 'document.pdf');
// Attachment structure
interface Attachment {
id: string;
type: 'image' | 'document';
fileName: string;
mimeType: string;
size: number;
content: string; // base64 encoded
extractedText?: string; // For documents
preview?: string; // base64 preview image
}
Supported formats: PDF, DOCX, XLSX, PPTX, images, text files.
For browser environments with CORS restrictions:
import { createStreamFn, shouldUseProxyForProvider, isCorsError } from '@mariozechner/pi-web-ui';
// AgentInterface auto-configures proxy from settings
// For manual setup:
agent.streamFn = createStreamFn(async () => {
const enabled = await storage.settings.get<boolean>('proxy.enabled');
return enabled ? await storage.settings.get<string>('proxy.url') : undefined;
});
// Providers requiring proxy:
// - zai: always
// - anthropic: only OAuth tokens (sk-ant-oat-*)
import { SettingsDialog, ProvidersModelsTab, ProxyTab, ApiKeysTab } from '@mariozechner/pi-web-ui';
SettingsDialog.open([
new ProvidersModelsTab(), // Custom providers + model list
new ProxyTab(), // CORS proxy settings
new ApiKeysTab(), // API keys per provider
]);
import { SessionListDialog } from '@mariozechner/pi-web-ui';
SessionListDialog.open(
async (sessionId) => { /* load session */ },
(deletedId) => { /* handle deletion */ },
);
import { ApiKeyPromptDialog } from '@mariozechner/pi-web-ui';
const success = await ApiKeyPromptDialog.prompt('anthropic');
import { ModelSelector } from '@mariozechner/pi-web-ui';
ModelSelector.open(currentModel, (selectedModel) => {
agent.state.model = selectedModel;
});
Import the pre-built CSS:
import '@mariozechner/pi-web-ui/app.css';
Or use Tailwind with custom config:
@import '@mariozechner/mini-lit/themes/claude.css';
@tailwind base;
@tailwind components;
@tailwind utilities;
import { i18n, setLanguage, translations } from '@mariozechner/pi-web-ui';
// Add translations
translations.de = {
'Loading...': 'Laden...',
'No sessions yet': 'Noch keine Sitzungen',
};
setLanguage('de');
console.log(i18n('Loading...')); // "Laden..."
MIT
FAQs
Reusable web UI components for AI chat interfaces powered by @mariozechner/pi-ai
The npm package @mariozechner/pi-web-ui receives a total of 4,889 weekly downloads. As such, @mariozechner/pi-web-ui popularity was classified as popular.
We found that @mariozechner/pi-web-ui demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 2 open source maintainers collaborating on the project.
Did you know?

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Security News
pnpm 11.5 now recognizes npm staged publish approvals in release metadata, preventing those releases from being mistaken for lower-trust package publishes.

Security News
Federal audit finds NIST lacked a plan to clear the NVD backlog, wasted funds on duplicate work, and delayed use of CISA data.

Research
/Security News
A mini Shai-Hulud campaign compromised Red Hat Cloud Services npm packages to steal developer and CI/CD secrets during installation.