@makemore/agent-frontend 2.10.0 → 2.11.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@makemore/agent-frontend",
3
- "version": "2.10.0",
3
+ "version": "2.11.0",
4
4
  "description": "A lightweight chat widget for AI agents. Use as an embeddable script tag or import directly into React/Preact projects.",
5
5
  "type": "module",
6
6
  "main": "dist/chat-widget.cjs.js",
@@ -0,0 +1,203 @@
1
+ /**
2
+ * ContentBlocks - renders structured content blocks from tool results.
3
+ *
4
+ * Each block type has a dedicated renderer. Unknown types are silently
5
+ * skipped so the system is forward-compatible with new block types that
6
+ * only newer clients understand.
7
+ */
8
+
9
+ import { html } from 'htm/preact';
10
+ import { useState } from 'preact/hooks';
11
+ import { escapeHtml, parseMarkdown } from '../utils/helpers.js';
12
+
13
+ // ---------------------------------------------------------------------------
14
+ // Individual block components
15
+ // ---------------------------------------------------------------------------
16
+
17
+ function CardBlock({ block, onAction, markdownParser }) {
18
+ return html`
19
+ <div class="cw-block-card">
20
+ ${block.image && html`<img class="cw-block-card-image" src=${block.image} alt=${block.title || ''} />`}
21
+ <div class="cw-block-card-body">
22
+ ${block.badge && html`<span class="cw-block-card-badge">${block.badge}</span>`}
23
+ ${block.title && html`<div class="cw-block-card-title">${block.title}</div>`}
24
+ ${block.subtitle && html`<div class="cw-block-card-subtitle">${block.subtitle}</div>`}
25
+ ${block.metadata && block.metadata.length > 0 && html`
26
+ <div class="cw-block-card-meta">
27
+ ${block.metadata.map(m => html`
28
+ <span class="cw-block-meta-pair">
29
+ <span class="cw-block-meta-label">${m.label}:</span> ${m.value}
30
+ </span>
31
+ `)}
32
+ </div>
33
+ `}
34
+ ${block.actions && block.actions.length > 0 && html`
35
+ <div class="cw-block-card-actions">
36
+ ${block.actions.map(a => html`<${ActionButton} action=${a} onAction=${onAction} />`)}
37
+ </div>
38
+ `}
39
+ </div>
40
+ </div>
41
+ `;
42
+ }
43
+
44
+ function CardListBlock({ block, onAction, markdownParser }) {
45
+ const layout = block.layout || 'vertical';
46
+ return html`
47
+ <div class="cw-block-card-list cw-block-card-list-${layout}">
48
+ ${(block.items || []).map(item => html`
49
+ <${CardBlock} block=${{ type: 'card', ...item }} onAction=${onAction} markdownParser=${markdownParser} />
50
+ `)}
51
+ </div>
52
+ `;
53
+ }
54
+
55
+ function ActionButton({ action, onAction }) {
56
+ const style = action.style || 'primary';
57
+ const handleClick = () => {
58
+ if (!onAction) return;
59
+ onAction(action);
60
+ };
61
+
62
+ if (action.type === 'link') {
63
+ return html`<a class="cw-block-btn cw-block-btn-${style}" href=${action.url} target="_blank" rel="noopener">${action.label}</a>`;
64
+ }
65
+ return html`<button class="cw-block-btn cw-block-btn-${style}" onClick=${handleClick}>${action.label}</button>`;
66
+ }
67
+
68
+ function ActionButtonsBlock({ block, onAction }) {
69
+ return html`
70
+ <div class="cw-block-action-buttons">
71
+ ${(block.buttons || []).map(a => html`<${ActionButton} action=${a} onAction=${onAction} />`)}
72
+ </div>
73
+ `;
74
+ }
75
+
76
+ function CalloutBlock({ block }) {
77
+ const style = block.style || 'info';
78
+ const icons = { info: 'ℹ️', success: '✅', warning: '⚠️' };
79
+ return html`
80
+ <div class="cw-block-callout cw-block-callout-${style}">
81
+ <span class="cw-block-callout-icon">${icons[style] || 'ℹ️'}</span>
82
+ <div class="cw-block-callout-content">
83
+ ${block.title && html`<strong>${block.title}</strong>`}
84
+ ${block.body && html`<span>${block.body}</span>`}
85
+ </div>
86
+ </div>
87
+ `;
88
+ }
89
+
90
+ function ImageBlock({ block }) {
91
+ return html`
92
+ <figure class="cw-block-image">
93
+ <img src=${block.url} alt=${block.alt || ''} />
94
+ ${block.caption && html`<figcaption>${block.caption}</figcaption>`}
95
+ </figure>
96
+ `;
97
+ }
98
+
99
+ function DividerBlock() {
100
+ return html`<hr class="cw-block-divider" />`;
101
+ }
102
+
103
+ function TableBlock({ block }) {
104
+ return html`
105
+ <div class="cw-block-table-wrapper">
106
+ <table class="cw-block-table">
107
+ ${block.headers && block.headers.length > 0 && html`
108
+ <thead><tr>${block.headers.map(h => html`<th>${h}</th>`)}</tr></thead>
109
+ `}
110
+ <tbody>
111
+ ${(block.rows || []).map(row => html`
112
+ <tr>${row.map(cell => html`<td>${cell}</td>`)}</tr>
113
+ `)}
114
+ </tbody>
115
+ </table>
116
+ </div>
117
+ `;
118
+ }
119
+
120
+ function CodeBlock({ block }) {
121
+ const [copied, setCopied] = useState(false);
122
+ const handleCopy = () => {
123
+ navigator.clipboard.writeText(block.code).then(() => {
124
+ setCopied(true);
125
+ setTimeout(() => setCopied(false), 1500);
126
+ });
127
+ };
128
+ return html`
129
+ <div class="cw-block-code">
130
+ ${block.filename && html`<div class="cw-block-code-filename">${block.filename}</div>`}
131
+ <pre><code>${escapeHtml(block.code)}</code></pre>
132
+ ${block.copyable !== false && html`
133
+ <button class="cw-block-code-copy" onClick=${handleCopy}>${copied ? '✓' : '⎘'}</button>
134
+ `}
135
+ </div>
136
+ `;
137
+ }
138
+
139
+ function CollapsibleBlock({ block }) {
140
+ const [open, setOpen] = useState(block.defaultOpen || false);
141
+ return html`
142
+ <details class="cw-block-collapsible" open=${open} onClick=${(e) => { e.preventDefault(); setOpen(!open); }}>
143
+ <summary>${block.title}</summary>
144
+ <div class="cw-block-collapsible-body">${block.body}</div>
145
+ </details>
146
+ `;
147
+ }
148
+
149
+ function StatusBlock({ block }) {
150
+ const icons = { loading: '⏳', success: '✅', error: '❌', warning: '⚠️', info: 'ℹ️' };
151
+ return html`
152
+ <div class="cw-block-status cw-block-status-${block.state || 'info'}">
153
+ <span class="cw-block-status-icon">${icons[block.state] || 'ℹ️'}</span>
154
+ <div>
155
+ <strong>${block.title}</strong>
156
+ ${block.body && html`<div>${block.body}</div>`}
157
+ ${block.progress != null && html`
158
+ <div class="cw-block-progress"><div class="cw-block-progress-bar" style=${{ width: `${block.progress * 100}%` }}></div></div>
159
+ `}
160
+ </div>
161
+ </div>
162
+ `;
163
+ }
164
+
165
+ // ---------------------------------------------------------------------------
166
+ // Registry & top-level renderer
167
+ // ---------------------------------------------------------------------------
168
+
169
+ const BLOCK_RENDERERS = {
170
+ card: CardBlock,
171
+ cardList: CardListBlock,
172
+ actionButtons: ActionButtonsBlock,
173
+ callout: CalloutBlock,
174
+ image: ImageBlock,
175
+ divider: DividerBlock,
176
+ table: TableBlock,
177
+ code: CodeBlock,
178
+ collapsible: CollapsibleBlock,
179
+ status: StatusBlock,
180
+ };
181
+
182
+ /**
183
+ * Render a list of content blocks.
184
+ *
185
+ * @param {Object} props
186
+ * @param {Array} props.blocks - Array of block objects with a `type` key
187
+ * @param {Function} props.onAction - Callback for message/callback actions
188
+ * @param {Object} props.markdownParser - Optional markdown parser instance
189
+ */
190
+ export function BlockRenderer({ blocks, onAction, markdownParser }) {
191
+ if (!blocks || blocks.length === 0) return null;
192
+
193
+ return html`
194
+ <div class="cw-content-blocks">
195
+ ${blocks.map((block, i) => {
196
+ const Comp = BLOCK_RENDERERS[block.type];
197
+ if (!Comp) return null;
198
+ return html`<${Comp} key=${i} block=${block} onAction=${onAction} markdownParser=${markdownParser} />`;
199
+ })}
200
+ </div>
201
+ `;
202
+ }
203
+
@@ -5,6 +5,7 @@
5
5
  import { html } from 'htm/preact';
6
6
  import { useState, useRef, useEffect } from 'preact/hooks';
7
7
  import { escapeHtml, parseMarkdown, formatFileSize, getFileTypeIcon } from '../utils/helpers.js';
8
+ import { BlockRenderer } from './ContentBlocks.js';
8
9
 
9
10
  // Debug payload viewer component
10
11
  function DebugPayload({ msg, show, onToggle }) {
@@ -152,6 +153,28 @@ export function Message({ msg, debugMode, markdownParser, onEdit, onRetry, isLoa
152
153
  `;
153
154
  }
154
155
 
156
+ // Content blocks: render rich structured UI elements
157
+ if (msg.type === 'content_blocks' && msg.metadata?.blocks) {
158
+ const handleBlockAction = (action) => {
159
+ if (action.type === 'message' && msg._onSendMessage) {
160
+ msg._onSendMessage(action.message);
161
+ }
162
+ if (action.type === 'callback' && msg._onCallback) {
163
+ msg._onCallback(action.callbackId);
164
+ }
165
+ };
166
+ return html`
167
+ <div class="cw-message-row" style="position: relative;">
168
+ <${BlockRenderer}
169
+ blocks=${msg.metadata.blocks}
170
+ onAction=${handleBlockAction}
171
+ markdownParser=${markdownParser}
172
+ />
173
+ ${debugMode && html`<${DebugPayload} msg=${msg} show=${showPayload} onToggle=${() => setShowPayload(!showPayload)} />`}
174
+ </div>
175
+ `;
176
+ }
177
+
155
178
  // Tool call/result: show compact inline version
156
179
  if (isToolCall || isToolResult) {
157
180
  const hasDetails = msg.metadata?.arguments || msg.metadata?.result;
@@ -169,6 +169,26 @@ export function useChat(config, api, storage) {
169
169
  } catch (err) { console.error('[ChatWidget] Parse error:', err); }
170
170
  });
171
171
 
172
+ // Handle content blocks from tool results (rich UI elements)
173
+ eventSource.addEventListener('content.blocks', (event) => {
174
+ try {
175
+ const data = JSON.parse(event.data);
176
+ if (config.onEvent) config.onEvent('content.blocks', data.payload);
177
+ setMessages(prev => [...prev, {
178
+ id: 'content-blocks-' + Date.now(),
179
+ role: 'assistant',
180
+ content: '',
181
+ timestamp: new Date(),
182
+ type: 'content_blocks',
183
+ metadata: {
184
+ toolName: data.payload.tool_name,
185
+ toolCallId: data.payload.tool_call_id,
186
+ blocks: data.payload.blocks,
187
+ },
188
+ }]);
189
+ } catch (err) { console.error('[ChatWidget] Parse error:', err); }
190
+ });
191
+
172
192
  const handleTerminal = (event) => {
173
193
  try {
174
194
  const data = JSON.parse(event.data);