@makemore/agent-frontend 2.10.0 → 2.11.1

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.1",
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",
@@ -305,6 +305,7 @@ export function ChatWidget({ config, onStateChange, markdownParser, apiRef }) {
305
305
  onLoadMore=${chat.loadMoreMessages}
306
306
  onEditMessage=${chat.editMessage}
307
307
  onRetryMessage=${chat.retryMessage}
308
+ onSendMessage=${handleSend}
308
309
  debugMode=${debugMode}
309
310
  markdownParser=${markdownParser}
310
311
  emptyStateTitle=${config.emptyStateTitle}
@@ -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 }) {
@@ -119,7 +120,7 @@ function InlineEditForm({ initialContent, onSave, onCancel }) {
119
120
  `;
120
121
  }
121
122
 
122
- export function Message({ msg, debugMode, markdownParser, onEdit, onRetry, isLoading, messageIndex }) {
123
+ export function Message({ msg, debugMode, markdownParser, onEdit, onRetry, onSendMessage, isLoading, messageIndex }) {
123
124
  const [expanded, setExpanded] = useState(false);
124
125
  const [showPayload, setShowPayload] = useState(false);
125
126
  const [isEditing, setIsEditing] = useState(false);
@@ -152,6 +153,31 @@ 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' && onSendMessage) {
160
+ onSendMessage(action.message);
161
+ }
162
+ if (action.type === 'callback' && msg._onCallback) {
163
+ msg._onCallback(action.callbackId);
164
+ }
165
+ if (action.type === 'link' && action.url) {
166
+ window.open(action.url, '_blank', 'noopener');
167
+ }
168
+ };
169
+ return html`
170
+ <div class="cw-message-row" style="position: relative;">
171
+ <${BlockRenderer}
172
+ blocks=${msg.metadata.blocks}
173
+ onAction=${handleBlockAction}
174
+ markdownParser=${markdownParser}
175
+ />
176
+ ${debugMode && html`<${DebugPayload} msg=${msg} show=${showPayload} onToggle=${() => setShowPayload(!showPayload)} />`}
177
+ </div>
178
+ `;
179
+ }
180
+
155
181
  // Tool call/result: show compact inline version
156
182
  if (isToolCall || isToolResult) {
157
183
  const hasDetails = msg.metadata?.arguments || msg.metadata?.result;
@@ -15,6 +15,7 @@ export function MessageList({
15
15
  onLoadMore,
16
16
  onEditMessage,
17
17
  onRetryMessage,
18
+ onSendMessage,
18
19
  debugMode,
19
20
  markdownParser,
20
21
  emptyStateTitle,
@@ -94,6 +95,7 @@ export function MessageList({
94
95
  markdownParser=${markdownParser}
95
96
  onEdit=${onEditMessage}
96
97
  onRetry=${onRetryMessage}
98
+ onSendMessage=${onSendMessage}
97
99
  isLoading=${isLoading}
98
100
  />
99
101
  `)}
@@ -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);