@makemore/agent-frontend 2.9.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/dist/chat-widget.cjs.js +330 -231
- package/dist/chat-widget.css +323 -1
- package/dist/chat-widget.esm.js +319 -220
- package/dist/chat-widget.js +285 -186
- package/dist/react.cjs.js +316 -217
- package/dist/react.esm.js +303 -204
- package/package.json +1 -1
- package/src/components/ChatWidget.js +18 -15
- package/src/components/ContentBlocks.js +203 -0
- package/src/components/Message.js +23 -0
- package/src/hooks/useChat.js +20 -0
- package/src/utils/config.js +3 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@makemore/agent-frontend",
|
|
3
|
-
"version": "2.
|
|
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",
|
|
@@ -243,21 +243,23 @@ export function ChatWidget({ config, onStateChange, markdownParser, apiRef }) {
|
|
|
243
243
|
/>
|
|
244
244
|
`}
|
|
245
245
|
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
246
|
+
${config.showHeader !== false && html`
|
|
247
|
+
<${Header}
|
|
248
|
+
config=${config}
|
|
249
|
+
debugMode=${debugMode}
|
|
250
|
+
isExpanded=${isExpanded}
|
|
251
|
+
isSpeaking=${isSpeaking}
|
|
252
|
+
messagesCount=${chat.messages.length}
|
|
253
|
+
isLoading=${chat.isLoading}
|
|
254
|
+
currentAgent=${currentAgent}
|
|
255
|
+
onClose=${() => setIsOpen(false)}
|
|
256
|
+
onToggleExpand=${() => setIsExpanded(!isExpanded)}
|
|
257
|
+
onToggleDebug=${() => setDebugMode(!debugMode)}
|
|
258
|
+
onToggleTTS=${() => setEnableTTS(!enableTTS)}
|
|
259
|
+
onClear=${chat.clearMessages}
|
|
260
|
+
onToggleSidebar=${handleToggleSidebar}
|
|
261
|
+
/>
|
|
262
|
+
`}
|
|
261
263
|
|
|
262
264
|
${config.showDevTools && html`
|
|
263
265
|
<${DevToolbar}
|
|
@@ -329,6 +331,7 @@ export function ChatWidget({ config, onStateChange, markdownParser, apiRef }) {
|
|
|
329
331
|
placeholder=${config.placeholder}
|
|
330
332
|
primaryColor=${config.primaryColor}
|
|
331
333
|
enableVoice=${config.enableVoice}
|
|
334
|
+
enableFiles=${config.showFileAttachment !== false}
|
|
332
335
|
/>
|
|
333
336
|
` : html`
|
|
334
337
|
<${TaskList}
|
|
@@ -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;
|
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);
|
package/src/utils/config.js
CHANGED
|
@@ -55,6 +55,9 @@ export const DEFAULT_CONFIG = {
|
|
|
55
55
|
theme: 'light',
|
|
56
56
|
|
|
57
57
|
// UI options
|
|
58
|
+
showHeader: true, // Show the header bar (title, action buttons). Set false for minimal chat.
|
|
59
|
+
showFileAttachment: true, // Show the file attachment button in the input area
|
|
60
|
+
showTasksTab: true, // Show the Chat/Tasks tab switcher
|
|
58
61
|
showConversationSidebar: true,
|
|
59
62
|
showClearButton: true,
|
|
60
63
|
showDebugButton: true,
|