@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/dist/chat-widget.cjs.js +374 -276
- package/dist/chat-widget.css +323 -1
- package/dist/chat-widget.esm.js +384 -286
- package/dist/chat-widget.js +302 -204
- package/dist/react.cjs.js +366 -268
- package/dist/react.esm.js +379 -281
- package/package.json +1 -1
- package/src/components/ChatWidget.js +1 -0
- package/src/components/ContentBlocks.js +203 -0
- package/src/components/Message.js +27 -1
- package/src/components/MessageList.js +2 -0
- package/src/hooks/useChat.js +20 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@makemore/agent-frontend",
|
|
3
|
-
"version": "2.
|
|
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
|
`)}
|
package/src/hooks/useChat.js
CHANGED
|
@@ -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);
|