@draht/web-ui

中文文档
2026.11.5发布于 2026/11/06MITTypeScriptES Module近 30 天下载 2189

Reusable web UI components for AI chat interfaces powered by @draht/ai

原始使用文档
文档目录36 节
  1. Features
  2. Installation
  3. Quick Start
  4. Architecture
  5. Components
  6. ChatPanel
  7. AgentInterface
  8. Agent (from @draht/agent-core)
  9. Message Types
  10. UserMessageWithAttachments
  11. ArtifactMessage
  12. Custom Message Types
  13. Message Transformer
  14. Tools
  15. JavaScript REPL
  16. Extract Document
  17. Artifacts Tool
  18. Custom Tool Renderers
  19. Storage
  20. Setup
  21. SettingsStore
  22. ProviderKeysStore
  23. SessionsStore
  24. CustomProvidersStore
  25. Attachments
  26. CORS Proxy
  27. Dialogs
  28. SettingsDialog
  29. SessionListDialog
  30. ApiKeyPromptDialog
  31. ModelSelector
  32. Styling
  33. Internationalization
  34. Examples
  35. Known Issues
  36. License

@draht/web-ui

Reusable web UI components for building AI chat interfaces powered by @draht/ai and @draht/agent-core.

Built with mini-lit web components and Tailwind CSS v4.

Features

  • Chat UI: Complete interface with message history, streaming, and tool execution
  • Tools: JavaScript REPL, document extraction, and artifacts (HTML, SVG, Markdown, etc.)
  • Attachments: PDF, DOCX, XLSX, PPTX, images with preview and text extraction
  • Artifacts: Interactive HTML, SVG, Markdown with sandboxed execution
  • Storage: IndexedDB-backed storage for sessions, API keys, and settings
  • CORS Proxy: Automatic proxy handling for browser environments
  • Custom Providers: Support for Ollama, LM Studio, vLLM, and OpenAI-compatible APIs

Installation

npm install @draht/web-ui @draht/agent-core @draht/ai

Quick Start

See the example directory for a complete working application.

import { Agent } from '@draht/agent-core';
import { getModel } from '@draht/ai';
import {
  ChatPanel,
  AppStorage,
  IndexedDBStorageBackend,
  ProviderKeysStore,
  SessionsStore,
  SettingsStore,
  setAppStorage,
  defaultConvertToLlm,
  ApiKeyPromptDialog,
} from '@draht/web-ui';
import '@draht/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-5'),
    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);

Architecture

┌─────────────────────────────────────────────────────┐
│                    ChatPanel                        │
│  ┌─────────────────────┐  ┌─────────────────────┐   │
│  │   AgentInterface    │  │   ArtifactsPanel    │   │
│  │  (messages, input)  │  │  (HTML, SVG, MD)    │   │
│  └─────────────────────┘  └─────────────────────┘   │
└─────────────────────────────────────────────────────┘
                          │
                          ▼
┌─────────────────────────────────────────────────────┐
│              Agent (from @draht/agent-core)             │
│  - State management (messages, model, tools)        │
│  - Event emission (agent_start, message_update, ...)│
│  - Tool execution                                   │
└─────────────────────────────────────────────────────┘
                          │
                          ▼
┌─────────────────────────────────────────────────────┐
│                   AppStorage                        │
│  ┌──────────┐ ┌──────────┐ ┌──────────┐             │
│  │ Settings │ │ Provider │ │ Sessions │             │
│  │  Store   │ │Keys Store│ │  Store   │             │
│  └──────────┘ └──────────┘ └──────────┘             │
│                     │                               │
│              IndexedDBStorageBackend                │
└─────────────────────────────────────────────────────┘

Components

ChatPanel

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];
  },
});

AgentInterface

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 instance
  • enableAttachments: Show attachment button (default: true)
  • enableModelSelector: Show model selector (default: true)
  • enableThinkingSelector: Show thinking level selector (default: true)
  • showThemeToggle: Show theme toggle (default: false)

Agent (from @draht/agent-core)

import { Agent } from '@draht/agent-core';

const agent = new Agent({
  initialState: {
    model: getModel('anthropic', 'claude-sonnet-5'),
    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);

Message Types

UserMessageWithAttachments

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);
}

ArtifactMessage

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);
}

Custom Message Types

Extend via declaration merging:

interface SystemNotification {
  role: 'system-notification';
  message: string;
  level: 'info' | 'warning' | 'error';
  timestamp: string;
}

declare module '@draht/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);
}

Message Transformer

convertToLlm transforms app messages to LLM-compatible format:

import { defaultConvertToLlm, convertAttachments } from '@draht/web-ui';

// defaultConvertToLlm handles:
// - UserMessageWithAttachments → user message with image/text content blocks
// - ArtifactMessage → filtered out (UI-only)
// - Standard messages (user, assistant, toolResult) → passed through

Tools

JavaScript REPL

Execute JavaScript in a sandboxed browser environment:

import { createJavaScriptReplTool } from '@draht/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 Document

Extract text from documents at URLs:

import { createExtractDocumentTool } from '@draht/web-ui';

const extractTool = createExtractDocumentTool();
extractTool.corsProxyUrl = 'https://corsproxy.io/?';

agent.state.tools = [extractTool];

Artifacts Tool

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];

Custom Tool Renderers

import { registerToolRenderer, type ToolRenderer } from '@draht/web-ui';

const myRenderer: ToolRenderer = {
  render(params, result, isStreaming) {
    return {
      content: html`<div>...</div>`,
      isCustom: false, // true = no card wrapper
    };
  },
};

registerToolRenderer('my_tool', myRenderer);

Storage

Setup

import {
  AppStorage,
  IndexedDBStorageBackend,
  SettingsStore,
  ProviderKeysStore,
  SessionsStore,
  CustomProvidersStore,
  setAppStorage,
  getAppStorage,
} from '@draht/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);

SettingsStore

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');

ProviderKeysStore

API keys by provider:

await storage.providerKeys.set('anthropic', 'sk-ant-...');
const key = await storage.providerKeys.get('anthropic');
const providers = await storage.providerKeys.list();

SessionsStore

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);

CustomProvidersStore

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();

Attachments

Load and process files:

import { loadAttachment, type Attachment } from '@draht/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.

CORS Proxy

For browser environments with CORS restrictions:

import { createStreamFn, shouldUseProxyForProvider, isCorsError } from '@draht/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-*)

Dialogs

SettingsDialog

import { SettingsDialog, ProvidersModelsTab, ProxyTab, ApiKeysTab } from '@draht/web-ui';

SettingsDialog.open([
  new ProvidersModelsTab(), // Custom providers + model list
  new ProxyTab(),           // CORS proxy settings
  new ApiKeysTab(),         // API keys per provider
]);

SessionListDialog

import { SessionListDialog } from '@draht/web-ui';

SessionListDialog.open(
  async (sessionId) => { /* load session */ },
  (deletedId) => { /* handle deletion */ },
);

ApiKeyPromptDialog

import { ApiKeyPromptDialog } from '@draht/web-ui';

const success = await ApiKeyPromptDialog.prompt('anthropic');

ModelSelector

import { ModelSelector } from '@draht/web-ui';

ModelSelector.open(currentModel, (selectedModel) => {
  agent.state.model = selectedModel;
});

Styling

Import the pre-built CSS:

import '@draht/web-ui/app.css';

Or use Tailwind with custom config:

@import '@mariozechner/mini-lit/themes/claude.css';
@tailwind base;
@tailwind components;
@tailwind utilities;

Internationalization

import { i18n, setLanguage, translations } from '@draht/web-ui';

// Add translations
translations.de = {
  'Loading...': 'Laden...',
  'No sessions yet': 'Noch keine Sitzungen',
};

setLanguage('de');
console.log(i18n('Loading...')); // "Laden..."

Examples

  • example/ - Complete web app with sessions, artifacts, custom messages
  • sitegeist - Browser extension using @draht/web-ui

Known Issues

  • PersistentStorageDialog: Currently broken

License

MIT

版本与发布记录

@draht/web-ui 共发布 50 个版本,首次发布于 2026/03/03;过去 12 个月发布了 50 个版本。

版本发布日期距今
2026.11.5latest2026/11/067 天前
2026.11.5-12026/11/058 天前
2026.10.312026/11/0112 天前
2026.10.31-12026/10/3113 天前
2026.10.232026/10/2420 天前
2026.10.23-12026/10/2321 天前

其他发布通道:next → 2026.3.2-4。

关于 @draht/web-ui 的常见问题

@draht/web-ui 是什么?

Reusable web UI components for AI chat interfaces powered by @draht/ai

如何安装 @draht/web-ui?

在项目目录运行 npm install @draht/web-ui,或使用 pnpm add / yarn add / bun add。它还需要宿主项目提供 peer 依赖:lit(^3.3.1)、@mariozechner/mini-lit(^0.2.0)。npm 7 及以上与 pnpm 8 及以上默认会自动安装必需的 peer 依赖,Yarn 需要手动安装。

@draht/web-ui 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@draht/web-ui 支持 ESM 还是 CommonJS?

以 ES Module 形式发布,推荐使用 import 引入。在 CommonJS 项目中,Node.js 20.19+ / 22.12+ 可以直接 require() 不含顶层 await 的 ES Module,更早的版本需要改用动态 import()。

@draht/web-ui 可以商用吗?

@draht/web-ui 使用 MIT 许可。宽松许可:允许商业使用、修改和再分发,通常只需在分发时保留原版权与许可声明。以上为概括,不构成法律意见,请以许可原文为准。

@draht/web-ui 的下载量有多少?

根据 npm 官方下载统计,@draht/web-ui 在 2026/10/13 – 2026/11/11 这 30 天内被下载 2,189 次,日均约 73 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@draht/web-ui 还在维护吗?

最新版本 2026.11.5 发布于 2026/11/06(7 天前);至今共发布 50 个版本,其中过去 12 个月发布了 50 个;首次发布于 2026/03/03。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。