校对:soup
用于构建 AI 聊天界面的可重复使用的 Web UI 组件 @draht/ai (英语) 财务报告和审定财务报表 @ draht/ 代理核心。 。 。 .
与 小号 网页组件和 Tailwind CSS v4.
特征
- 聊天用户界面: 与消息历史、流出和工具执行完成接口
- 工具:JavaScript RePL,文件提取,和文物(HTML,SVG,Markdown等)
- 附录: PDF, DOCX, XLSX, PPTX, 有预览和文本提取的图像
- 艺术品: 交互式 HTML, SVG, 有沙盒执行的马克下
- 存储: 用于会话、 API 密钥和设置的索引化 DB 备份存储
- CORS 代理服务器: 浏览器环境的自动代理处理
- 自定义提供者: 支持 Ollama, LM Studio, vLLM, 和 OpenAI 兼容的 API
安装
npm install @draht/web-ui @draht/agent-core @draht/ai
快速启动
见 实例 完整的工作应用程序目录.
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);
建筑
┌─────────────────────────────────────────────────────┐
│ 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 │
└─────────────────────────────────────────────────────┘
构成部分
聊天小组
与内置文物面板的高层次聊天界面.
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];
},
});
代理界面
用于自定义布局的下层聊天界面.
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 () => { /* ... */ };
属性 :
session: 代理实例enableAttachments: 显示附件按钮( 默认: 真)enableModelSelector: 显示模式选择器( 默认: 真)enableThinkingSelector: 显示思维级别选择器( 默认: 真)showThemeToggle: 显示主题切换( 默认: 假)
(来自@draht/代理核心)
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);
信件类型
用户信息
文件附件的用户信件 :
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);
}
人工行为
用于工艺品的持久性:
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);
}
自定义信件类型
通过声明合并扩展 :
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);
}
信件变形器
convertToLlm 将应用程序消息转换为 LLM 兼容格式 :
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
工具
Java脚本 REPL
在沙盒浏览器环境中执行 JavaScript :
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];
提取文档
在 URL 从文档中提取文本 :
import { createExtractDocumentTool } from '@draht/web-ui';
const extractTool = createExtractDocumentTool();
extractTool.corsProxyUrl = 'https://corsproxy.io/?';
agent.state.tools = [extractTool];
艺术工具
被建入ArtifactsPanel,支持:HTML,SVG,Markdown,文本,JSON,图像,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 '@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);
存储
设置
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);
设置
密钥值设置 :
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');
提供商KeysStore
API 按提供者键 :
await storage.providerKeys.set('anthropic', 'sk-ant-...');
const key = await storage.providerKeys.get('anthropic');
const providers = await storage.providerKeys.list();
会话Store
用元数据聊天会话 :
// 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);
自定义提供程序
自定义 LLM 提供者 :
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();
附录
装入和处理文件 :
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
}
所支持的格式:PDF,DOCX,XLSX,PPTX,图像,文本文件.
CORS 代理服务器
对于具有 CORS 限制的浏览器环境:
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-*)
对话框
设置对话框
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
]);
会话列表
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');
模型选择器
import { ModelSelector } from '@draht/web-ui';
ModelSelector.open(currentModel, (selectedModel) => {
agent.state.model = selectedModel;
});
样式
导入预建的 CSS :
import '@draht/web-ui/app.css';
或使用自定义配置的 Tailwind:
@import '@mariozechner/mini-lit/themes/claude.css';
@tailwind base;
@tailwind components;
@tailwind utilities;
国际化
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..."
实例
已知问题
- 常态成像仪: 已断开
许可证
麻省理工学院