@makemore/agent-frontend 2.7.2 → 2.8.3

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,21 +1,34 @@
1
1
  {
2
2
  "name": "@makemore/agent-frontend",
3
- "version": "2.7.2",
4
- "description": "A lightweight chat widget for AI agents built with Preact. Embed conversational AI into any website with a single script tag.",
3
+ "version": "2.8.3",
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
- "main": "dist/chat-widget.js",
7
- "module": "src/index.js",
6
+ "main": "dist/chat-widget.cjs.js",
7
+ "module": "dist/chat-widget.esm.js",
8
+ "browser": "dist/chat-widget.js",
9
+ "exports": {
10
+ ".": {
11
+ "import": "./dist/chat-widget.esm.js",
12
+ "require": "./dist/chat-widget.cjs.js",
13
+ "browser": "./dist/chat-widget.js"
14
+ },
15
+ "./react": {
16
+ "import": "./dist/react.esm.js",
17
+ "require": "./dist/react.cjs.js"
18
+ },
19
+ "./embed": "./dist/chat-widget.js",
20
+ "./css": "./dist/chat-widget.css"
21
+ },
8
22
  "files": [
9
- "dist/chat-widget.js",
10
- "dist/chat-widget.css",
11
- "dist/chat-widget-markdown.js",
23
+ "dist/",
12
24
  "src/",
13
25
  "README.md",
14
26
  "LICENSE"
15
27
  ],
16
28
  "scripts": {
17
- "build": "esbuild src/index.js --bundle --minify --format=iife --global-name=ChatWidgetModule --outfile=dist/chat-widget.js && npm run copy",
18
- "build:dev": "esbuild src/index.js --bundle --format=iife --global-name=ChatWidgetModule --outfile=dist/chat-widget.js --sourcemap && npm run copy",
29
+ "build": "node build.js",
30
+ "build:dev": "node build.js --dev",
31
+ "build:embed": "esbuild src/index.js --bundle --minify --format=iife --global-name=ChatWidgetModule --outfile=dist/chat-widget.js",
19
32
  "watch": "node watch.js",
20
33
  "copy": "cp -f dist/chat-widget.js ../django_agent_studio/static/agent-frontend/chat-widget.js 2>/dev/null || true && echo 'Copied to django_agent_studio'",
21
34
  "prepublishOnly": "npm run build",
@@ -52,6 +65,18 @@
52
65
  "htm": "^3.1.1",
53
66
  "preact": "^10.19.3"
54
67
  },
68
+ "peerDependencies": {
69
+ "react": ">=16.8.0",
70
+ "react-dom": ">=16.8.0"
71
+ },
72
+ "peerDependenciesMeta": {
73
+ "react": {
74
+ "optional": true
75
+ },
76
+ "react-dom": {
77
+ "optional": true
78
+ }
79
+ },
55
80
  "devDependencies": {
56
81
  "esbuild": "^0.20.0"
57
82
  }
@@ -14,9 +14,10 @@ import { useChat } from '../hooks/useChat.js';
14
14
  import { useModels } from '../hooks/useModels.js';
15
15
  import { useTasks } from '../hooks/useTasks.js';
16
16
  import { createApiClient } from '../utils/api.js';
17
- import { createStorage } from '../utils/helpers.js';
17
+ import { createStorage, getContrastingTextColor } from '../utils/helpers.js';
18
18
 
19
19
  export function ChatWidget({ config, onStateChange, markdownParser, apiRef }) {
20
+ console.log('[ChatWidget] Config:', { showConversationSidebar: config.showConversationSidebar, apiPaths: config.apiPaths });
20
21
  // UI state
21
22
  const [isOpen, setIsOpen] = useState(config.embedded || config.forceOpen === true);
22
23
  const [isExpanded, setIsExpanded] = useState(false);
@@ -86,7 +87,10 @@ export function ChatWidget({ config, onStateChange, markdownParser, apiRef }) {
86
87
  // Load initial conversation if stored
87
88
  useEffect(() => {
88
89
  const storedConvId = storage.get(config.conversationIdKey);
90
+ console.log('[ChatWidget] Initial load - storedConvId:', storedConvId, 'key:', config.conversationIdKey);
91
+ console.log('[ChatWidget] apiPaths.conversations:', config.apiPaths.conversations);
89
92
  if (storedConvId) {
93
+ console.log('[ChatWidget] Loading conversation:', storedConvId);
90
94
  chat.loadConversation(storedConvId);
91
95
  }
92
96
  }, []);
@@ -149,6 +153,7 @@ export function ChatWidget({ config, onStateChange, markdownParser, apiRef }) {
149
153
  const handleSend = useCallback((content) => {
150
154
  chat.sendMessage(content, {
151
155
  model: models.selectedModel,
156
+ thinking: models.thinkingEnabled && models.supportsThinking(),
152
157
  onAssistantMessage: (assistantContent) => {
153
158
  // TTS callback when assistant finishes
154
159
  if (enableTTS && assistantContent) {
@@ -156,7 +161,7 @@ export function ChatWidget({ config, onStateChange, markdownParser, apiRef }) {
156
161
  }
157
162
  }
158
163
  });
159
- }, [chat, enableTTS, models.selectedModel]);
164
+ }, [chat, enableTTS, models.selectedModel, models.thinkingEnabled, models.supportsThinking]);
160
165
 
161
166
  // Handle tab switching
162
167
  const handleTabChange = useCallback((tab) => {
@@ -205,8 +210,11 @@ export function ChatWidget({ config, onStateChange, markdownParser, apiRef }) {
205
210
  config.embedded && 'cw-widget-embedded',
206
211
  ].filter(Boolean).join(' ');
207
212
 
213
+ // Calculate header text color for contrast
214
+ const headerTextColor = config.headerTextColor || getContrastingTextColor(config.primaryColor);
215
+
208
216
  return html`
209
- <div class=${widgetClasses} style=${{ '--cw-primary': config.primaryColor }}>
217
+ <div class=${widgetClasses} style=${{ '--cw-primary': config.primaryColor, '--cw-header-text': headerTextColor }}>
210
218
  ${config.showConversationSidebar && html`
211
219
  <${Sidebar}
212
220
  isOpen=${sidebarOpen}
@@ -276,6 +284,8 @@ export function ChatWidget({ config, onStateChange, markdownParser, apiRef }) {
276
284
  availableModels=${models.availableModels}
277
285
  selectedModel=${models.selectedModel}
278
286
  onSelectModel=${models.selectModel}
287
+ thinkingEnabled=${models.thinkingEnabled}
288
+ onToggleThinking=${models.toggleThinking}
279
289
  disabled=${chat.isLoading}
280
290
  />
281
291
  `}
@@ -1,16 +1,18 @@
1
1
  /**
2
- * ModelSelector component - dropdown for selecting LLM model
2
+ * ModelSelector component - dropdown for selecting LLM model with thinking toggle
3
3
  */
4
4
 
5
5
  import { html } from 'htm/preact';
6
6
  import { useState } from 'preact/hooks';
7
7
  import { escapeHtml } from '../utils/helpers.js';
8
8
 
9
- export function ModelSelector({
10
- availableModels,
11
- selectedModel,
9
+ export function ModelSelector({
10
+ availableModels,
11
+ selectedModel,
12
12
  onSelectModel,
13
- disabled
13
+ thinkingEnabled,
14
+ onToggleThinking,
15
+ disabled
14
16
  }) {
15
17
  const [isOpen, setIsOpen] = useState(false);
16
18
 
@@ -20,6 +22,7 @@ export function ModelSelector({
20
22
 
21
23
  const selectedModelInfo = availableModels.find(m => m.id === selectedModel);
22
24
  const displayName = selectedModelInfo?.name || 'Select Model';
25
+ const supportsThinking = selectedModelInfo?.supports_thinking || false;
23
26
 
24
27
  const handleToggle = () => {
25
28
  if (!disabled) {
@@ -32,10 +35,17 @@ export function ModelSelector({
32
35
  setIsOpen(false);
33
36
  };
34
37
 
38
+ const handleThinkingToggle = (e) => {
39
+ e.stopPropagation();
40
+ if (onToggleThinking && supportsThinking) {
41
+ onToggleThinking(!thinkingEnabled);
42
+ }
43
+ };
44
+
35
45
  return html`
36
46
  <div class="cw-model-selector">
37
- <button
38
- class="cw-model-btn"
47
+ <button
48
+ class="cw-model-btn"
39
49
  onClick=${handleToggle}
40
50
  disabled=${disabled}
41
51
  title="Select Model"
@@ -44,16 +54,30 @@ export function ModelSelector({
44
54
  <span class="cw-model-name">${escapeHtml(displayName)}</span>
45
55
  <span class="cw-model-chevron">${isOpen ? '▲' : '▼'}</span>
46
56
  </button>
47
-
57
+
58
+ ${supportsThinking && onToggleThinking && html`
59
+ <button
60
+ class="cw-thinking-toggle ${thinkingEnabled ? 'cw-thinking-enabled' : ''}"
61
+ onClick=${handleThinkingToggle}
62
+ disabled=${disabled}
63
+ title=${thinkingEnabled ? 'Thinking enabled - click to disable' : 'Enable extended thinking'}
64
+ >
65
+ <span class="cw-thinking-icon">🧠</span>
66
+ </button>
67
+ `}
68
+
48
69
  ${isOpen && html`
49
70
  <div class="cw-model-dropdown">
50
71
  ${availableModels.map(model => html`
51
- <button
72
+ <button
52
73
  key=${model.id}
53
74
  class="cw-model-option ${model.id === selectedModel ? 'cw-model-option-selected' : ''}"
54
75
  onClick=${() => handleSelect(model.id)}
55
76
  >
56
- <span class="cw-model-option-name">${escapeHtml(model.name)}</span>
77
+ <span class="cw-model-option-name">
78
+ ${escapeHtml(model.name)}
79
+ ${model.supports_thinking && html`<span class="cw-thinking-badge" title="Supports extended thinking">🧠</span>`}
80
+ </span>
57
81
  <span class="cw-model-option-provider">${escapeHtml(model.provider)}</span>
58
82
  ${model.description && html`
59
83
  <span class="cw-model-option-desc">${escapeHtml(model.description)}</span>
@@ -224,7 +224,7 @@ export function useChat(config, api, storage) {
224
224
  options = optionsOrFiles || {};
225
225
  }
226
226
 
227
- const { model, onAssistantMessage, supersedeFromMessageIndex } = options;
227
+ const { model, thinking, onAssistantMessage, supersedeFromMessageIndex } = options;
228
228
 
229
229
  setIsLoading(true);
230
230
  setError(null);
@@ -271,6 +271,10 @@ export function useChat(config, api, storage) {
271
271
  formData.append('model', model);
272
272
  }
273
273
 
274
+ if (thinking) {
275
+ formData.append('thinking', 'true');
276
+ }
277
+
274
278
  // Append each file
275
279
  files.forEach(file => {
276
280
  formData.append('files', file);
@@ -289,6 +293,7 @@ export function useChat(config, api, storage) {
289
293
  messages: [{ role: 'user', content: content.trim() }],
290
294
  metadata: { ...config.metadata, journeyType: config.defaultJourneyType },
291
295
  ...(model && { model }),
296
+ ...(thinking && { thinking: true }),
292
297
  // Edit/retry support: tell backend to mark old runs as superseded
293
298
  ...(supersedeFromMessageIndex !== undefined && { supersedeFromMessageIndex }),
294
299
  });
@@ -446,6 +451,7 @@ export function useChat(config, api, storage) {
446
451
  };
447
452
 
448
453
  const loadConversation = useCallback(async (convId) => {
454
+ console.log('[ChatWidget] loadConversation called with:', convId);
449
455
  setIsLoading(true);
450
456
  setMessages([]);
451
457
  setConversationId(convId);
@@ -454,21 +460,30 @@ export function useChat(config, api, storage) {
454
460
  const token = await api.getOrCreateSession();
455
461
  const limit = 10;
456
462
  const url = `${config.backendUrl}${config.apiPaths.conversations}${convId}/?limit=${limit}&offset=0`;
463
+ console.log('[ChatWidget] Fetching conversation from:', url);
457
464
 
458
465
  const response = await fetch(url, api.getFetchOptions({ method: 'GET' }, token));
466
+ console.log('[ChatWidget] Response status:', response.status);
459
467
 
460
468
  if (response.ok) {
461
469
  const rawConversation = await response.json();
470
+ console.log('[ChatWidget] Raw conversation:', rawConversation);
462
471
  const conversation = api.transformResponse(rawConversation);
472
+ console.log('[ChatWidget] Transformed conversation:', conversation);
463
473
  if (conversation.messages) {
464
474
  // Use flatMap to handle tool_calls which return arrays, filter out nulls (empty messages)
465
- setMessages(conversation.messages.flatMap(mapApiMessage).filter(Boolean));
475
+ const mappedMessages = conversation.messages.flatMap(mapApiMessage).filter(Boolean);
476
+ console.log('[ChatWidget] Mapped messages:', mappedMessages);
477
+ setMessages(mappedMessages);
466
478
  }
467
479
  setHasMoreMessages(conversation.hasMore || false);
468
480
  setMessagesOffset(conversation.messages?.length || 0);
469
481
  } else if (response.status === 404) {
482
+ console.log('[ChatWidget] Conversation not found, clearing');
470
483
  setConversationId(null);
471
484
  storage?.set(config.conversationIdKey, null);
485
+ } else {
486
+ console.error('[ChatWidget] Unexpected response status:', response.status);
472
487
  }
473
488
  } catch (err) {
474
489
  console.error('[ChatWidget] Failed to load conversation:', err);
@@ -1,33 +1,36 @@
1
1
  /**
2
- * Models hook - manages available models and selection
2
+ * Models hook - manages available models, selection, and thinking mode
3
3
  */
4
4
 
5
5
  import { useState, useEffect, useCallback } from 'preact/hooks';
6
6
 
7
+ const THINKING_KEY = 'cw_thinking_enabled';
8
+
7
9
  export function useModels(config, api, storage) {
8
10
  const [availableModels, setAvailableModels] = useState([]);
9
11
  const [selectedModel, setSelectedModel] = useState(null);
10
12
  const [defaultModel, setDefaultModel] = useState(null);
11
13
  const [isLoading, setIsLoading] = useState(false);
14
+ const [thinkingEnabled, setThinkingEnabled] = useState(false);
12
15
 
13
16
  // Load available models on mount
14
17
  useEffect(() => {
15
18
  const loadModels = async () => {
16
19
  if (!config.showModelSelector) return;
17
-
20
+
18
21
  setIsLoading(true);
19
22
  try {
20
23
  const response = await fetch(
21
24
  `${config.backendUrl}${config.apiPaths.models}`,
22
25
  api.getFetchOptions({ method: 'GET' })
23
26
  );
24
-
27
+
25
28
  if (response.ok) {
26
29
  const data = await response.json();
27
30
  const models = data.models || [];
28
31
  setAvailableModels(models);
29
32
  setDefaultModel(data.default);
30
-
33
+
31
34
  // Restore saved model or use default
32
35
  const savedModel = storage?.get(config.modelKey);
33
36
  if (savedModel && models.some(m => m.id === savedModel)) {
@@ -35,6 +38,12 @@ export function useModels(config, api, storage) {
35
38
  } else {
36
39
  setSelectedModel(data.default);
37
40
  }
41
+
42
+ // Restore thinking preference
43
+ const savedThinking = storage?.get(THINKING_KEY);
44
+ if (savedThinking === 'true') {
45
+ setThinkingEnabled(true);
46
+ }
38
47
  }
39
48
  } catch (err) {
40
49
  console.warn('[ChatWidget] Failed to load models:', err);
@@ -52,11 +61,23 @@ export function useModels(config, api, storage) {
52
61
  storage?.set(config.modelKey, modelId);
53
62
  }, [config.modelKey, storage]);
54
63
 
64
+ // Toggle thinking mode
65
+ const toggleThinking = useCallback((enabled) => {
66
+ setThinkingEnabled(enabled);
67
+ storage?.set(THINKING_KEY, enabled ? 'true' : 'false');
68
+ }, [storage]);
69
+
55
70
  // Get the currently selected model object
56
71
  const getSelectedModelInfo = useCallback(() => {
57
72
  return availableModels.find(m => m.id === selectedModel) || null;
58
73
  }, [availableModels, selectedModel]);
59
74
 
75
+ // Check if current model supports thinking
76
+ const supportsThinking = useCallback(() => {
77
+ const model = getSelectedModelInfo();
78
+ return model?.supports_thinking || false;
79
+ }, [getSelectedModelInfo]);
80
+
60
81
  return {
61
82
  availableModels,
62
83
  selectedModel,
@@ -64,6 +85,9 @@ export function useModels(config, api, storage) {
64
85
  isLoading,
65
86
  selectModel,
66
87
  getSelectedModelInfo,
88
+ thinkingEnabled,
89
+ toggleThinking,
90
+ supportsThinking,
67
91
  };
68
92
  }
69
93
 
package/src/react.js ADDED
@@ -0,0 +1,78 @@
1
+ /**
2
+ * React-compatible exports for @makemore/agent-frontend
3
+ *
4
+ * This module provides React-compatible components and hooks.
5
+ * It uses preact/compat under the hood, which provides full React API compatibility.
6
+ *
7
+ * Usage in React projects:
8
+ *
9
+ * import { ChatWidget, useChat, useModels } from '@makemore/agent-frontend/react';
10
+ *
11
+ * function App() {
12
+ * return (
13
+ * <ChatWidget
14
+ * config={{
15
+ * backendUrl: 'https://api.example.com',
16
+ * agentKey: 'my-agent',
17
+ * }}
18
+ * />
19
+ * );
20
+ * }
21
+ *
22
+ * Or use hooks directly for custom UI:
23
+ *
24
+ * import { useChat, createApiClient, mergeConfig } from '@makemore/agent-frontend/react';
25
+ *
26
+ * function CustomChat() {
27
+ * const config = mergeConfig({ backendUrl: '...', agentKey: '...' });
28
+ * const api = createApiClient(config, () => ({}), () => {});
29
+ * const chat = useChat(config, api, null);
30
+ *
31
+ * return (
32
+ * <div>
33
+ * {chat.messages.map(msg => <div key={msg.id}>{msg.content}</div>)}
34
+ * <button onClick={() => chat.sendMessage('Hello!')}>Send</button>
35
+ * </div>
36
+ * );
37
+ * }
38
+ */
39
+
40
+ // Components
41
+ export { ChatWidget } from './components/ChatWidget.js';
42
+ export { Header } from './components/Header.js';
43
+ export { MessageList } from './components/MessageList.js';
44
+ export { Message } from './components/Message.js';
45
+ export { InputForm } from './components/InputForm.js';
46
+ export { Sidebar } from './components/Sidebar.js';
47
+ export { ModelSelector } from './components/ModelSelector.js';
48
+ export { TaskList } from './components/TaskList.js';
49
+
50
+ // Hooks
51
+ export { useChat } from './hooks/useChat.js';
52
+ export { useModels } from './hooks/useModels.js';
53
+ export { useTasks } from './hooks/useTasks.js';
54
+
55
+ // Utilities
56
+ export { createApiClient } from './utils/api.js';
57
+ export { mergeConfig, DEFAULT_CONFIG } from './utils/config.js';
58
+ export {
59
+ generateId,
60
+ createStorage,
61
+ camelToSnake,
62
+ snakeToCamel,
63
+ keysToCamel,
64
+ keysToSnake,
65
+ parseMarkdown,
66
+ formatDate,
67
+ formatFileSize,
68
+ getFileTypeIcon,
69
+ getCSRFToken,
70
+ } from './utils/helpers.js';
71
+
72
+ // Re-export the imperative API for backwards compatibility
73
+ export { ChatWidget as ChatWidgetAPI } from './index.js';
74
+
75
+ // Default export is the main ChatWidget component
76
+ import { ChatWidget as ChatWidgetComponent } from './components/ChatWidget.js';
77
+ export default ChatWidgetComponent;
78
+
@@ -8,6 +8,7 @@ export const DEFAULT_CONFIG = {
8
8
  title: 'Chat Assistant',
9
9
  subtitle: 'How can we help you today?',
10
10
  primaryColor: '#0066cc',
11
+ headerTextColor: null, // Auto-detect based on primaryColor luminance, or set explicitly
11
12
  position: 'bottom-right',
12
13
  defaultJourneyType: 'general',
13
14
  enableDebugMode: true,
@@ -173,3 +173,38 @@ export function getFileTypeIcon(mimeType) {
173
173
  if (mimeType.includes('text/')) return '📄';
174
174
  return '📄';
175
175
  }
176
+
177
+ /**
178
+ * Calculate relative luminance of a hex color
179
+ * Returns a value between 0 (black) and 1 (white)
180
+ */
181
+ export function getLuminance(hexColor) {
182
+ if (!hexColor || typeof hexColor !== 'string') return 0;
183
+
184
+ // Remove # if present
185
+ const hex = hexColor.replace('#', '');
186
+ if (hex.length !== 6 && hex.length !== 3) return 0;
187
+
188
+ // Expand 3-char hex to 6-char
189
+ const fullHex = hex.length === 3
190
+ ? hex.split('').map(c => c + c).join('')
191
+ : hex;
192
+
193
+ const r = parseInt(fullHex.substr(0, 2), 16) / 255;
194
+ const g = parseInt(fullHex.substr(2, 2), 16) / 255;
195
+ const b = parseInt(fullHex.substr(4, 2), 16) / 255;
196
+
197
+ // sRGB luminance formula
198
+ const toLinear = (c) => c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
199
+ return 0.2126 * toLinear(r) + 0.7152 * toLinear(g) + 0.0722 * toLinear(b);
200
+ }
201
+
202
+ /**
203
+ * Get contrasting text color (black or white) for a given background color
204
+ */
205
+ export function getContrastingTextColor(bgColor) {
206
+ const luminance = getLuminance(bgColor);
207
+ // Use white text for dark backgrounds, black for light backgrounds
208
+ // Threshold of 0.179 is based on WCAG contrast ratio guidelines
209
+ return luminance > 0.179 ? '#000000' : '#ffffff';
210
+ }