@makemore/agent-frontend 2.3.0 → 2.7.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.3.0",
3
+ "version": "2.7.0",
4
4
  "description": "A lightweight chat widget for AI agents built with Preact. Embed conversational AI into any website with a single script tag.",
5
5
  "type": "module",
6
6
  "main": "dist/chat-widget.js",
@@ -9,8 +9,10 @@ import { MessageList } from './MessageList.js';
9
9
  import { InputForm } from './InputForm.js';
10
10
  import { Sidebar } from './Sidebar.js';
11
11
  import { ModelSelector } from './ModelSelector.js';
12
+ import { TaskList } from './TaskList.js';
12
13
  import { useChat } from '../hooks/useChat.js';
13
14
  import { useModels } from '../hooks/useModels.js';
15
+ import { useTasks } from '../hooks/useTasks.js';
14
16
  import { createApiClient } from '../utils/api.js';
15
17
  import { createStorage } from '../utils/helpers.js';
16
18
 
@@ -21,6 +23,7 @@ export function ChatWidget({ config, onStateChange, markdownParser, apiRef }) {
21
23
  const [debugMode, setDebugMode] = useState(false);
22
24
  const [sidebarOpen, setSidebarOpen] = useState(false);
23
25
  const [conversations, setConversations] = useState([]);
26
+ const [activeTab, setActiveTab] = useState('chat'); // 'chat' or 'tasks'
24
27
  const [conversationsLoading, setConversationsLoading] = useState(false);
25
28
 
26
29
  // TTS state
@@ -57,6 +60,9 @@ export function ChatWidget({ config, onStateChange, markdownParser, apiRef }) {
57
60
  // Models hook
58
61
  const models = useModels(config, api, storage);
59
62
 
63
+ // Tasks hook
64
+ const tasks = useTasks(config, api);
65
+
60
66
  // Track current agent from messages (for multi-agent systems)
61
67
  useEffect(() => {
62
68
  // Look for the most recent sub_agent_start or sub_agent_end message
@@ -152,6 +158,14 @@ export function ChatWidget({ config, onStateChange, markdownParser, apiRef }) {
152
158
  });
153
159
  }, [chat, enableTTS, models.selectedModel]);
154
160
 
161
+ // Handle tab switching
162
+ const handleTabChange = useCallback((tab) => {
163
+ setActiveTab(tab);
164
+ if (tab === 'tasks') {
165
+ tasks.loadTaskList();
166
+ }
167
+ }, [tasks]);
168
+
155
169
  // Expose imperative API to parent
156
170
  useEffect(() => {
157
171
  if (apiRef) {
@@ -221,39 +235,71 @@ export function ChatWidget({ config, onStateChange, markdownParser, apiRef }) {
221
235
  onToggleSidebar=${handleToggleSidebar}
222
236
  />
223
237
 
238
+ ${config.showTasksTab !== false && html`
239
+ <div class="cw-tabs">
240
+ <button
241
+ class=${`cw-tab ${activeTab === 'chat' ? 'cw-tab-active' : ''}`}
242
+ onClick=${() => handleTabChange('chat')}
243
+ >
244
+ Chat
245
+ </button>
246
+ <button
247
+ class=${`cw-tab ${activeTab === 'tasks' ? 'cw-tab-active' : ''}`}
248
+ onClick=${() => handleTabChange('tasks')}
249
+ >
250
+ Tasks ${tasks.progress.total > 0 ? html`<span class="cw-tab-badge">${tasks.progress.completed}/${tasks.progress.total}</span>` : ''}
251
+ </button>
252
+ </div>
253
+ `}
254
+
224
255
  ${debugMode && html`<div class="cw-status-bar"><span>🐛 Debug</span></div>`}
225
256
 
226
- <${MessageList}
227
- messages=${chat.messages}
228
- isLoading=${chat.isLoading}
229
- hasMoreMessages=${chat.hasMoreMessages}
230
- loadingMoreMessages=${chat.loadingMoreMessages}
231
- onLoadMore=${chat.loadMoreMessages}
232
- debugMode=${debugMode}
233
- markdownParser=${markdownParser}
234
- emptyStateTitle=${config.emptyStateTitle}
235
- emptyStateMessage=${config.emptyStateMessage}
236
- />
257
+ ${activeTab === 'chat' ? html`
258
+ <${MessageList}
259
+ messages=${chat.messages}
260
+ isLoading=${chat.isLoading}
261
+ hasMoreMessages=${chat.hasMoreMessages}
262
+ loadingMoreMessages=${chat.loadingMoreMessages}
263
+ onLoadMore=${chat.loadMoreMessages}
264
+ onEditMessage=${chat.editMessage}
265
+ onRetryMessage=${chat.retryMessage}
266
+ debugMode=${debugMode}
267
+ markdownParser=${markdownParser}
268
+ emptyStateTitle=${config.emptyStateTitle}
269
+ emptyStateMessage=${config.emptyStateMessage}
270
+ />
237
271
 
238
- ${chat.error && html`<div class="cw-error-bar">${chat.error}</div>`}
272
+ ${chat.error && html`<div class="cw-error-bar">${chat.error}</div>`}
239
273
 
240
- ${config.showModelSelector && models.availableModels.length > 0 && html`
241
- <${ModelSelector}
242
- availableModels=${models.availableModels}
243
- selectedModel=${models.selectedModel}
244
- onSelectModel=${models.selectModel}
245
- disabled=${chat.isLoading}
274
+ ${config.showModelSelector && models.availableModels.length > 0 && html`
275
+ <${ModelSelector}
276
+ availableModels=${models.availableModels}
277
+ selectedModel=${models.selectedModel}
278
+ onSelectModel=${models.selectModel}
279
+ disabled=${chat.isLoading}
280
+ />
281
+ `}
282
+
283
+ <${InputForm}
284
+ onSend=${handleSend}
285
+ onCancel=${chat.cancelRun}
286
+ isLoading=${chat.isLoading}
287
+ placeholder=${config.placeholder}
288
+ primaryColor=${config.primaryColor}
289
+ enableVoice=${config.enableVoice}
290
+ />
291
+ ` : html`
292
+ <${TaskList}
293
+ tasks=${tasks.tasks}
294
+ progress=${tasks.progress}
295
+ isLoading=${tasks.isLoading}
296
+ error=${tasks.error}
297
+ onUpdate=${tasks.updateTask}
298
+ onRemove=${tasks.removeTask}
299
+ onClear=${tasks.clearTasks}
300
+ onRefresh=${tasks.loadTaskList}
246
301
  />
247
302
  `}
248
-
249
- <${InputForm}
250
- onSend=${handleSend}
251
- onCancel=${chat.cancelRun}
252
- isLoading=${chat.isLoading}
253
- placeholder=${config.placeholder}
254
- primaryColor=${config.primaryColor}
255
- enableVoice=${config.enableVoice}
256
- />
257
303
  </div>
258
304
  `;
259
305
  }
@@ -3,7 +3,7 @@
3
3
  */
4
4
 
5
5
  import { html } from 'htm/preact';
6
- import { useState } from 'preact/hooks';
6
+ import { useState, useRef, useEffect } from 'preact/hooks';
7
7
  import { escapeHtml, parseMarkdown, formatFileSize, getFileTypeIcon } from '../utils/helpers.js';
8
8
 
9
9
  // Debug payload viewer component
@@ -26,9 +26,103 @@ function DebugPayload({ msg, show, onToggle }) {
26
26
  `;
27
27
  }
28
28
 
29
- export function Message({ msg, debugMode, markdownParser }) {
29
+ // Edit/Retry action buttons for user messages
30
+ function MessageActions({ onEdit, onRetry, isLoading, position, showEdit = true }) {
31
+ if (isLoading) return null;
32
+
33
+ return html`
34
+ <div class="cw-message-actions cw-message-actions-${position || 'left'}">
35
+ ${showEdit && html`
36
+ <button
37
+ class="cw-message-action-btn"
38
+ onClick=${onEdit}
39
+ title="Edit message"
40
+ >
41
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
42
+ <path d="M17 3a2.828 2.828 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5L17 3z"></path>
43
+ </svg>
44
+ </button>
45
+ `}
46
+ <button
47
+ class="cw-message-action-btn"
48
+ onClick=${onRetry}
49
+ title="Retry from here"
50
+ >
51
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
52
+ <path d="M21 2v6h-6"></path>
53
+ <path d="M3 12a9 9 0 0 1 15-6.7L21 8"></path>
54
+ <path d="M3 22v-6h6"></path>
55
+ <path d="M21 12a9 9 0 0 1-15 6.7L3 16"></path>
56
+ </svg>
57
+ </button>
58
+ </div>
59
+ `;
60
+ }
61
+
62
+ // Inline edit form for editing a message
63
+ function InlineEditForm({ initialContent, onSave, onCancel }) {
64
+ const [content, setContent] = useState(initialContent);
65
+ const textareaRef = useRef(null);
66
+
67
+ useEffect(() => {
68
+ if (textareaRef.current) {
69
+ textareaRef.current.focus();
70
+ textareaRef.current.setSelectionRange(content.length, content.length);
71
+ // Auto-resize
72
+ textareaRef.current.style.height = 'auto';
73
+ textareaRef.current.style.height = textareaRef.current.scrollHeight + 'px';
74
+ }
75
+ }, []);
76
+
77
+ const handleInput = (e) => {
78
+ setContent(e.target.value);
79
+ // Auto-resize textarea
80
+ e.target.style.height = 'auto';
81
+ e.target.style.height = e.target.scrollHeight + 'px';
82
+ };
83
+
84
+ const handleKeyDown = (e) => {
85
+ if (e.key === 'Enter' && !e.shiftKey) {
86
+ e.preventDefault();
87
+ if (content.trim()) {
88
+ onSave(content.trim());
89
+ }
90
+ } else if (e.key === 'Escape') {
91
+ onCancel();
92
+ }
93
+ };
94
+
95
+ return html`
96
+ <div class="cw-inline-edit">
97
+ <textarea
98
+ ref=${textareaRef}
99
+ class="cw-inline-edit-input"
100
+ value=${content}
101
+ onInput=${handleInput}
102
+ onKeyDown=${handleKeyDown}
103
+ rows="1"
104
+ />
105
+ <div class="cw-inline-edit-actions">
106
+ <button
107
+ class="cw-inline-edit-btn cw-inline-edit-cancel"
108
+ onClick=${onCancel}
109
+ title="Cancel (Esc)"
110
+ >Cancel</button>
111
+ <button
112
+ class="cw-inline-edit-btn cw-inline-edit-save"
113
+ onClick=${() => content.trim() && onSave(content.trim())}
114
+ disabled=${!content.trim()}
115
+ title="Save & Resend (Enter)"
116
+ >Save & Send</button>
117
+ </div>
118
+ </div>
119
+ `;
120
+ }
121
+
122
+ export function Message({ msg, debugMode, markdownParser, onEdit, onRetry, isLoading, messageIndex }) {
30
123
  const [expanded, setExpanded] = useState(false);
31
124
  const [showPayload, setShowPayload] = useState(false);
125
+ const [isEditing, setIsEditing] = useState(false);
32
126
 
33
127
  const isUser = msg.role === 'user';
34
128
  const isSystem = msg.role === 'system';
@@ -139,10 +233,68 @@ export function Message({ msg, debugMode, markdownParser }) {
139
233
  `;
140
234
  };
141
235
 
236
+ // Handle edit save - always triggers a new run with the (possibly edited) content
237
+ const handleEditSave = (newContent) => {
238
+ setIsEditing(false);
239
+ if (onEdit) {
240
+ onEdit(messageIndex, newContent);
241
+ }
242
+ };
243
+
244
+ // Handle retry (resend same message)
245
+ const handleRetry = () => {
246
+ if (onRetry) {
247
+ onRetry(messageIndex);
248
+ }
249
+ };
250
+
251
+ // Show inline edit form for user messages in edit mode
252
+ if (isUser && isEditing) {
253
+ return html`
254
+ <div class=${rowClasses} style="position: relative;">
255
+ ${renderAttachments()}
256
+ <${InlineEditForm}
257
+ initialContent=${msg.content}
258
+ onSave=${handleEditSave}
259
+ onCancel=${() => setIsEditing(false)}
260
+ />
261
+ </div>
262
+ `;
263
+ }
264
+
265
+ // Show edit/retry actions for user messages (on left side since user messages are on right)
266
+ const showUserActions = isUser && onEdit && onRetry;
267
+ // Show retry-only for assistant messages (below the message)
268
+ const isAssistant = msg.role === 'assistant';
269
+ const showAssistantRetry = isAssistant && onRetry && !isLoading;
270
+ const hasActions = showUserActions || showAssistantRetry;
271
+
142
272
  return html`
143
- <div class=${rowClasses} style="position: relative;">
273
+ <div class="${rowClasses} ${hasActions ? 'cw-message-row-with-actions' : ''}">
144
274
  ${renderAttachments()}
145
- <div class=${classes} dangerouslySetInnerHTML=${{ __html: content }} />
275
+ ${showUserActions && html`
276
+ <div class="cw-user-actions-wrapper">
277
+ <${MessageActions}
278
+ onEdit=${() => setIsEditing(true)}
279
+ onRetry=${handleRetry}
280
+ isLoading=${isLoading}
281
+ position="left"
282
+ showEdit=${true}
283
+ />
284
+ <div class=${classes} dangerouslySetInnerHTML=${{ __html: content }} />
285
+ </div>
286
+ `}
287
+ ${!showUserActions && html`
288
+ <div class=${classes} dangerouslySetInnerHTML=${{ __html: content }} />
289
+ `}
290
+ ${showAssistantRetry && html`
291
+ <${MessageActions}
292
+ onRetry=${handleRetry}
293
+ isLoading=${isLoading}
294
+ position="right"
295
+ showEdit=${false}
296
+ />
297
+ `}
146
298
  ${debugMode && html`<${DebugPayload} msg=${msg} show=${showPayload} onToggle=${() => setShowPayload(!showPayload)} />`}
147
299
  </div>
148
300
  `;
@@ -13,6 +13,8 @@ export function MessageList({
13
13
  hasMoreMessages,
14
14
  loadingMoreMessages,
15
15
  onLoadMore,
16
+ onEditMessage,
17
+ onRetryMessage,
16
18
  debugMode,
17
19
  markdownParser,
18
20
  emptyStateTitle,
@@ -83,12 +85,16 @@ export function MessageList({
83
85
  </div>
84
86
  `}
85
87
 
86
- ${messages.map(msg => html`
87
- <${Message}
88
- key=${msg.id}
89
- msg=${msg}
88
+ ${messages.map((msg, index) => html`
89
+ <${Message}
90
+ key=${msg.id}
91
+ msg=${msg}
92
+ messageIndex=${index}
90
93
  debugMode=${debugMode}
91
94
  markdownParser=${markdownParser}
95
+ onEdit=${onEditMessage}
96
+ onRetry=${onRetryMessage}
97
+ isLoading=${isLoading}
92
98
  />
93
99
  `)}
94
100
 
@@ -0,0 +1,183 @@
1
+ /**
2
+ * TaskList component - displays and manages the user's task list
3
+ */
4
+
5
+ import { html } from 'htm/preact';
6
+ import { useState, useCallback } from 'preact/hooks';
7
+
8
+ // State icons for tasks
9
+ const STATE_ICONS = {
10
+ not_started: '○',
11
+ in_progress: '◐',
12
+ complete: '●',
13
+ cancelled: '⊘',
14
+ };
15
+
16
+ const STATE_LABELS = {
17
+ not_started: 'Not Started',
18
+ in_progress: 'In Progress',
19
+ complete: 'Complete',
20
+ cancelled: 'Cancelled',
21
+ };
22
+
23
+ function TaskItem({ task, onUpdate, onRemove, depth = 0 }) {
24
+ const [isEditing, setIsEditing] = useState(false);
25
+ const [editName, setEditName] = useState(task.name);
26
+
27
+ const handleStateToggle = useCallback(() => {
28
+ // Cycle through states: not_started -> in_progress -> complete
29
+ const nextState = {
30
+ not_started: 'in_progress',
31
+ in_progress: 'complete',
32
+ complete: 'not_started',
33
+ cancelled: 'not_started',
34
+ };
35
+ onUpdate(task.id, { state: nextState[task.state] || 'not_started' });
36
+ }, [task, onUpdate]);
37
+
38
+ const handleSaveEdit = useCallback(() => {
39
+ if (editName.trim() && editName !== task.name) {
40
+ onUpdate(task.id, { name: editName.trim() });
41
+ }
42
+ setIsEditing(false);
43
+ }, [task, editName, onUpdate]);
44
+
45
+ const handleKeyDown = useCallback((e) => {
46
+ if (e.key === 'Enter') handleSaveEdit();
47
+ if (e.key === 'Escape') {
48
+ setEditName(task.name);
49
+ setIsEditing(false);
50
+ }
51
+ }, [handleSaveEdit, task.name]);
52
+
53
+ const stateClass = `cw-task-state-${task.state.replace('_', '-')}`;
54
+
55
+ return html`
56
+ <div class="cw-task-item ${stateClass}" style=${{ paddingLeft: `${depth * 16 + 8}px` }}>
57
+ <button
58
+ class="cw-task-state-btn"
59
+ onClick=${handleStateToggle}
60
+ title=${STATE_LABELS[task.state]}
61
+ >
62
+ ${STATE_ICONS[task.state]}
63
+ </button>
64
+
65
+ ${isEditing ? html`
66
+ <input
67
+ type="text"
68
+ class="cw-task-edit-input"
69
+ value=${editName}
70
+ onInput=${(e) => setEditName(e.target.value)}
71
+ onBlur=${handleSaveEdit}
72
+ onKeyDown=${handleKeyDown}
73
+ autoFocus
74
+ />
75
+ ` : html`
76
+ <span
77
+ class="cw-task-name"
78
+ onClick=${() => setIsEditing(true)}
79
+ title="Click to edit"
80
+ >
81
+ ${task.name}
82
+ </span>
83
+ `}
84
+
85
+ <button
86
+ class="cw-task-remove-btn"
87
+ onClick=${() => onRemove(task.id)}
88
+ title="Remove task"
89
+ >
90
+ ×
91
+ </button>
92
+ </div>
93
+ `;
94
+ }
95
+
96
+ export function TaskList({
97
+ tasks,
98
+ progress,
99
+ isLoading,
100
+ error,
101
+ onUpdate,
102
+ onRemove,
103
+ onClear,
104
+ onRefresh
105
+ }) {
106
+ // Build tree structure from flat tasks
107
+ const buildTree = useCallback((tasks) => {
108
+ const taskMap = new Map();
109
+ const roots = [];
110
+
111
+ // First pass: create map
112
+ tasks.forEach(task => {
113
+ taskMap.set(task.id, { ...task, children: [] });
114
+ });
115
+
116
+ // Second pass: build tree
117
+ tasks.forEach(task => {
118
+ const node = taskMap.get(task.id);
119
+ if (task.parent_id && taskMap.has(task.parent_id)) {
120
+ taskMap.get(task.parent_id).children.push(node);
121
+ } else {
122
+ roots.push(node);
123
+ }
124
+ });
125
+
126
+ return roots;
127
+ }, []);
128
+
129
+ const renderTask = useCallback((task, depth = 0) => {
130
+ return html`
131
+ <${TaskItem}
132
+ key=${task.id}
133
+ task=${task}
134
+ depth=${depth}
135
+ onUpdate=${onUpdate}
136
+ onRemove=${onRemove}
137
+ />
138
+ ${task.children?.map(child => renderTask(child, depth + 1))}
139
+ `;
140
+ }, [onUpdate, onRemove]);
141
+
142
+ const taskTree = buildTree(tasks);
143
+
144
+ if (isLoading && tasks.length === 0) {
145
+ return html`<div class="cw-tasks-loading">Loading tasks...</div>`;
146
+ }
147
+
148
+ return html`
149
+ <div class="cw-tasks-container">
150
+ <div class="cw-tasks-header">
151
+ <div class="cw-tasks-progress">
152
+ <span class="cw-tasks-progress-text">
153
+ ${progress.completed}/${progress.total} complete
154
+ </span>
155
+ <div class="cw-tasks-progress-bar">
156
+ <div
157
+ class="cw-tasks-progress-fill"
158
+ style=${{ width: `${progress.percent_complete}%` }}
159
+ />
160
+ </div>
161
+ </div>
162
+ <div class="cw-tasks-actions">
163
+ <button class="cw-tasks-action-btn" onClick=${onRefresh} title="Refresh">↻</button>
164
+ ${tasks.length > 0 && html`
165
+ <button class="cw-tasks-action-btn" onClick=${onClear} title="Clear all">🗑</button>
166
+ `}
167
+ </div>
168
+ </div>
169
+
170
+ ${error && html`<div class="cw-tasks-error">${error}</div>`}
171
+
172
+ <div class="cw-tasks-list">
173
+ ${taskTree.length === 0 ? html`
174
+ <div class="cw-tasks-empty">
175
+ <p>No tasks yet</p>
176
+ <p class="cw-tasks-empty-hint">Tasks will appear here when the agent creates them</p>
177
+ </div>
178
+ ` : taskTree.map(task => renderTask(task))}
179
+ </div>
180
+ </div>
181
+ `;
182
+ }
183
+