@makemore/agent-frontend 1.8.0 → 2.0.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.
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Models hook - manages available models and selection
3
+ */
4
+
5
+ import { useState, useEffect, useCallback } from 'preact/hooks';
6
+
7
+ export function useModels(config, api, storage) {
8
+ const [availableModels, setAvailableModels] = useState([]);
9
+ const [selectedModel, setSelectedModel] = useState(null);
10
+ const [defaultModel, setDefaultModel] = useState(null);
11
+ const [isLoading, setIsLoading] = useState(false);
12
+
13
+ // Load available models on mount
14
+ useEffect(() => {
15
+ const loadModels = async () => {
16
+ if (!config.showModelSelector) return;
17
+
18
+ setIsLoading(true);
19
+ try {
20
+ const response = await fetch(
21
+ `${config.backendUrl}${config.apiPaths.models}`,
22
+ api.getFetchOptions({ method: 'GET' })
23
+ );
24
+
25
+ if (response.ok) {
26
+ const data = await response.json();
27
+ const models = data.models || [];
28
+ setAvailableModels(models);
29
+ setDefaultModel(data.default);
30
+
31
+ // Restore saved model or use default
32
+ const savedModel = storage?.get(config.modelKey);
33
+ if (savedModel && models.some(m => m.id === savedModel)) {
34
+ setSelectedModel(savedModel);
35
+ } else {
36
+ setSelectedModel(data.default);
37
+ }
38
+ }
39
+ } catch (err) {
40
+ console.warn('[ChatWidget] Failed to load models:', err);
41
+ } finally {
42
+ setIsLoading(false);
43
+ }
44
+ };
45
+
46
+ loadModels();
47
+ }, [config.backendUrl, config.apiPaths.models, config.showModelSelector, config.modelKey, api, storage]);
48
+
49
+ // Select a model
50
+ const selectModel = useCallback((modelId) => {
51
+ setSelectedModel(modelId);
52
+ storage?.set(config.modelKey, modelId);
53
+ }, [config.modelKey, storage]);
54
+
55
+ // Get the currently selected model object
56
+ const getSelectedModelInfo = useCallback(() => {
57
+ return availableModels.find(m => m.id === selectedModel) || null;
58
+ }, [availableModels, selectedModel]);
59
+
60
+ return {
61
+ availableModels,
62
+ selectedModel,
63
+ defaultModel,
64
+ isLoading,
65
+ selectModel,
66
+ getSelectedModelInfo,
67
+ };
68
+ }
69
+
package/src/index.js ADDED
@@ -0,0 +1,222 @@
1
+ /**
2
+ * Chat Widget - Main Entry Point
3
+ *
4
+ * Provides the same public API as the original vanilla JS version:
5
+ * - ChatWidget.init(config) - Initialize singleton instance
6
+ * - ChatWidget.createInstance(config) - Create multiple instances
7
+ * - ChatWidget.open/close/send/etc - Control the widget
8
+ */
9
+
10
+ import { render, h } from 'preact';
11
+ import { html } from 'htm/preact';
12
+ import { ChatWidget as ChatWidgetComponent } from './components/ChatWidget.js';
13
+ import { mergeConfig } from './utils/config.js';
14
+
15
+ // Track instances
16
+ const instances = new Map();
17
+ let instanceCounter = 0;
18
+ let defaultInstance = null;
19
+
20
+ /**
21
+ * Widget instance wrapper - provides imperative API around the Preact component
22
+ */
23
+ class ChatWidgetInstance {
24
+ constructor(userConfig = {}) {
25
+ this.instanceId = `cw-${++instanceCounter}`;
26
+ this.config = mergeConfig(userConfig);
27
+ this.container = null;
28
+ this._state = {};
29
+ this._apiRef = { current: null };
30
+
31
+ instances.set(this.instanceId, this);
32
+ }
33
+
34
+ _handleStateChange = (state) => {
35
+ this._state = state;
36
+ };
37
+
38
+ init() {
39
+ if (this.config.containerId) {
40
+ // Embedded mode: use existing container
41
+ this.container = document.getElementById(this.config.containerId);
42
+ if (!this.container) {
43
+ console.error(`[ChatWidget] Container not found: ${this.config.containerId}`);
44
+ return this;
45
+ }
46
+ this.container.classList.add('cw-container-embedded');
47
+ } else {
48
+ // Floating mode: create container
49
+ this.container = document.createElement('div');
50
+ this.container.id = this.instanceId;
51
+ this.container.className = `cw-container cw-position-${this.config.position}`;
52
+ document.body.appendChild(this.container);
53
+ }
54
+
55
+ // Render the Preact component
56
+ this._render();
57
+
58
+ console.log(`[ChatWidget] Instance ${this.instanceId} initialized`);
59
+ return this;
60
+ }
61
+
62
+ _render(configOverrides = {}) {
63
+ if (!this.container) return;
64
+ render(
65
+ html`<${ChatWidgetComponent}
66
+ config=${{ ...this.config, ...configOverrides }}
67
+ onStateChange=${this._handleStateChange}
68
+ markdownParser=${ChatWidget._enhancedMarkdownParser}
69
+ apiRef=${this._apiRef}
70
+ />`,
71
+ this.container
72
+ );
73
+ }
74
+
75
+ destroy() {
76
+ if (this.container) {
77
+ render(null, this.container);
78
+
79
+ if (this.config.containerId) {
80
+ this.container.classList.remove('cw-container-embedded');
81
+ } else {
82
+ this.container.remove();
83
+ }
84
+ this.container = null;
85
+ }
86
+
87
+ instances.delete(this.instanceId);
88
+ console.log(`[ChatWidget] Instance ${this.instanceId} destroyed`);
89
+ }
90
+
91
+ // Public API methods - delegate to component via apiRef
92
+ open() {
93
+ if (this._apiRef.current) {
94
+ this._apiRef.current.open();
95
+ } else {
96
+ this._render({ forceOpen: true });
97
+ }
98
+ }
99
+
100
+ close() {
101
+ if (this._apiRef.current) {
102
+ this._apiRef.current.close();
103
+ } else {
104
+ this._render({ forceOpen: false });
105
+ }
106
+ }
107
+
108
+ send(message) {
109
+ if (this._apiRef.current) {
110
+ this._apiRef.current.send(message);
111
+ }
112
+ }
113
+
114
+ clearMessages() {
115
+ if (this._apiRef.current) {
116
+ this._apiRef.current.clearMessages();
117
+ }
118
+ }
119
+
120
+ toggleTTS() {
121
+ if (this._apiRef.current) {
122
+ this._apiRef.current.toggleTTS();
123
+ }
124
+ }
125
+
126
+ stopSpeech() {
127
+ if (this._apiRef.current) {
128
+ this._apiRef.current.stopSpeech();
129
+ }
130
+ }
131
+
132
+ setAuth(config) {
133
+ if (this._apiRef.current) {
134
+ this._apiRef.current.setAuth(config);
135
+ }
136
+ }
137
+
138
+ clearAuth() {
139
+ if (this._apiRef.current) {
140
+ this._apiRef.current.clearAuth();
141
+ }
142
+ }
143
+
144
+ getState() {
145
+ return { ...this._state };
146
+ }
147
+
148
+ getConfig() {
149
+ return { ...this.config };
150
+ }
151
+ }
152
+
153
+ // ============================================================================
154
+ // Public API
155
+ // ============================================================================
156
+
157
+ function createInstance(config = {}) {
158
+ const instance = new ChatWidgetInstance(config);
159
+ return instance.init();
160
+ }
161
+
162
+ function init(config = {}) {
163
+ if (defaultInstance) {
164
+ defaultInstance.destroy();
165
+ }
166
+ defaultInstance = createInstance(config);
167
+ return defaultInstance;
168
+ }
169
+
170
+ function destroy() {
171
+ if (defaultInstance) {
172
+ defaultInstance.destroy();
173
+ defaultInstance = null;
174
+ }
175
+ }
176
+
177
+ // Proxy methods to default instance
178
+ function open() { if (defaultInstance) defaultInstance.open(); }
179
+ function close() { if (defaultInstance) defaultInstance.close(); }
180
+ function send(message) { if (defaultInstance) defaultInstance.send(message); }
181
+ function clearMessages() { if (defaultInstance) defaultInstance.clearMessages(); }
182
+ function toggleTTS() { if (defaultInstance) defaultInstance.toggleTTS(); }
183
+ function stopSpeech() { if (defaultInstance) defaultInstance.stopSpeech(); }
184
+ function setAuth(config) { if (defaultInstance) defaultInstance.setAuth(config); }
185
+ function clearAuth() { if (defaultInstance) defaultInstance.clearAuth(); }
186
+ function getState() { return defaultInstance ? defaultInstance.getState() : null; }
187
+ function getConfig() { return defaultInstance ? defaultInstance.getConfig() : null; }
188
+
189
+ // Export public API
190
+ const ChatWidget = {
191
+ // Multi-instance API
192
+ createInstance,
193
+ getInstance: (id) => instances.get(id),
194
+ getAllInstances: () => Array.from(instances.values()),
195
+
196
+ // Singleton API (backwards compatible)
197
+ init,
198
+ destroy,
199
+ open,
200
+ close,
201
+ send,
202
+ clearMessages,
203
+ toggleTTS,
204
+ stopSpeech,
205
+ setAuth,
206
+ clearAuth,
207
+ getState,
208
+ getConfig,
209
+
210
+ // For markdown plugin
211
+ _enhancedMarkdownParser: null,
212
+ };
213
+
214
+ // Export for both module and global usage
215
+ export { ChatWidget };
216
+ export default ChatWidget;
217
+
218
+ // Also attach to window for script tag usage
219
+ if (typeof window !== 'undefined') {
220
+ window.ChatWidget = ChatWidget;
221
+ }
222
+
@@ -0,0 +1,90 @@
1
+ /**
2
+ * API utilities for the chat widget
3
+ */
4
+
5
+ import { getCSRFToken } from './helpers.js';
6
+
7
+ export function createApiClient(config, getState, setState) {
8
+ const getAuthStrategy = () => {
9
+ if (config.authStrategy) return config.authStrategy;
10
+ if (config.authToken) return 'token';
11
+ if (config.apiPaths.anonymousSession || config.anonymousSessionEndpoint) return 'anonymous';
12
+ return 'none';
13
+ };
14
+
15
+ const getAuthHeaders = () => {
16
+ const strategy = getAuthStrategy();
17
+ const headers = {};
18
+ const token = config.authToken || getState().authToken;
19
+
20
+ if (strategy === 'token' && token) {
21
+ const headerName = config.authHeader || 'Authorization';
22
+ const prefix = config.authTokenPrefix !== undefined ? config.authTokenPrefix : 'Token';
23
+ headers[headerName] = prefix ? `${prefix} ${token}` : token;
24
+ } else if (strategy === 'jwt' && token) {
25
+ const headerName = config.authHeader || 'Authorization';
26
+ const prefix = config.authTokenPrefix !== undefined ? config.authTokenPrefix : 'Bearer';
27
+ headers[headerName] = prefix ? `${prefix} ${token}` : token;
28
+ } else if (strategy === 'anonymous' && token) {
29
+ const headerName = config.authHeader || config.anonymousTokenHeader || 'X-Anonymous-Token';
30
+ headers[headerName] = token;
31
+ }
32
+
33
+ if (strategy === 'session') {
34
+ const csrfToken = getCSRFToken(config.csrfCookieName);
35
+ if (csrfToken) {
36
+ headers['X-CSRFToken'] = csrfToken;
37
+ }
38
+ }
39
+
40
+ return headers;
41
+ };
42
+
43
+ const getFetchOptions = (options = {}) => {
44
+ const strategy = getAuthStrategy();
45
+ const fetchOptions = { ...options };
46
+ fetchOptions.headers = { ...fetchOptions.headers, ...getAuthHeaders() };
47
+ if (strategy === 'session') fetchOptions.credentials = 'include';
48
+ return fetchOptions;
49
+ };
50
+
51
+ const getOrCreateSession = async () => {
52
+ const strategy = getAuthStrategy();
53
+ const state = getState();
54
+
55
+ if (strategy !== 'anonymous') return config.authToken || state.authToken;
56
+ if (state.authToken) return state.authToken;
57
+
58
+ const storageKey = config.anonymousTokenKey || config.sessionTokenKey;
59
+ const stored = state.storage?.get(storageKey);
60
+ if (stored) {
61
+ setState(s => ({ ...s, authToken: stored }));
62
+ return stored;
63
+ }
64
+
65
+ try {
66
+ const endpoint = config.anonymousSessionEndpoint || config.apiPaths.anonymousSession;
67
+ const response = await fetch(`${config.backendUrl}${endpoint}`, {
68
+ method: 'POST',
69
+ headers: { 'Content-Type': 'application/json' },
70
+ });
71
+ if (response.ok) {
72
+ const data = await response.json();
73
+ setState(s => ({ ...s, authToken: data.token }));
74
+ state.storage?.set(storageKey, data.token);
75
+ return data.token;
76
+ }
77
+ } catch (e) {
78
+ console.warn('[ChatWidget] Failed to create session:', e);
79
+ }
80
+ return null;
81
+ };
82
+
83
+ return {
84
+ getAuthStrategy,
85
+ getAuthHeaders,
86
+ getFetchOptions,
87
+ getOrCreateSession,
88
+ };
89
+ }
90
+
@@ -0,0 +1,83 @@
1
+ /**
2
+ * Default configuration for the chat widget
3
+ */
4
+
5
+ export const DEFAULT_CONFIG = {
6
+ backendUrl: 'http://localhost:8000',
7
+ agentKey: 'default-agent',
8
+ title: 'Chat Assistant',
9
+ subtitle: 'How can we help you today?',
10
+ primaryColor: '#0066cc',
11
+ position: 'bottom-right',
12
+ defaultJourneyType: 'general',
13
+ enableDebugMode: true,
14
+ enableAutoRun: true,
15
+ journeyTypes: {},
16
+ customerPrompts: {},
17
+ placeholder: 'Type your message...',
18
+ emptyStateTitle: 'Start a Conversation',
19
+ emptyStateMessage: 'Send a message to get started.',
20
+
21
+ // Auth
22
+ authStrategy: null,
23
+ authToken: null,
24
+ authHeader: null,
25
+ authTokenPrefix: null,
26
+ anonymousSessionEndpoint: null,
27
+ anonymousTokenKey: 'chat_widget_anonymous_token',
28
+ onAuthError: null,
29
+ anonymousTokenHeader: 'X-Anonymous-Token',
30
+ conversationIdKey: 'chat_widget_conversation_id',
31
+ sessionTokenKey: 'chat_widget_session_token',
32
+
33
+ // API paths
34
+ apiPaths: {
35
+ anonymousSession: '/api/accounts/anonymous-session/',
36
+ conversations: '/api/agent-runtime/conversations/',
37
+ runs: '/api/agent-runtime/runs/',
38
+ runEvents: '/api/agent-runtime/runs/{runId}/events/',
39
+ simulateCustomer: '/api/agent-runtime/simulate-customer/',
40
+ ttsVoices: '/api/tts/voices/',
41
+ ttsSetVoice: '/api/tts/set-voice/',
42
+ models: '/api/agent-runtime/models/',
43
+ },
44
+
45
+ // UI options
46
+ showConversationSidebar: true,
47
+ showClearButton: true,
48
+ showDebugButton: true,
49
+ showTTSButton: true,
50
+ showVoiceSettings: true,
51
+ showExpandButton: true,
52
+ showModelSelector: false,
53
+
54
+ // Model selection
55
+ modelKey: 'chat_widget_selected_model',
56
+
57
+ // Auto-run
58
+ autoRunDelay: 1000,
59
+ autoRunMode: 'automatic',
60
+
61
+ // TTS
62
+ enableTTS: false,
63
+ ttsProxyUrl: null,
64
+ elevenLabsApiKey: null,
65
+ ttsVoices: { assistant: null, user: null },
66
+ ttsModel: 'eleven_turbo_v2_5',
67
+ ttsSettings: { stability: 0.5, similarity_boost: 0.75, style: 0.0, use_speaker_boost: true },
68
+ availableVoices: [],
69
+
70
+ // Callbacks
71
+ onEvent: null,
72
+
73
+ // Multi-instance options
74
+ containerId: null,
75
+ embedded: false,
76
+ metadata: {},
77
+ };
78
+
79
+ export function mergeConfig(userConfig) {
80
+ const mergedApiPaths = { ...DEFAULT_CONFIG.apiPaths, ...(userConfig.apiPaths || {}) };
81
+ return { ...DEFAULT_CONFIG, ...userConfig, apiPaths: mergedApiPaths };
82
+ }
83
+
@@ -0,0 +1,83 @@
1
+ /**
2
+ * Utility functions for the chat widget
3
+ */
4
+
5
+ export function generateId() {
6
+ return 'msg-' + Date.now() + '-' + Math.random().toString(36).substr(2, 9);
7
+ }
8
+
9
+ export function escapeHtml(text) {
10
+ const div = document.createElement('div');
11
+ div.textContent = text;
12
+ return div.innerHTML;
13
+ }
14
+
15
+ export function formatDate(dateStr) {
16
+ if (!dateStr) return '';
17
+ try {
18
+ const date = new Date(dateStr);
19
+ const now = new Date();
20
+ const diffMs = now - date;
21
+ const diffMins = Math.floor(diffMs / 60000);
22
+ const diffHours = Math.floor(diffMs / 3600000);
23
+ const diffDays = Math.floor(diffMs / 86400000);
24
+
25
+ if (diffMins < 1) return 'Just now';
26
+ if (diffMins < 60) return `${diffMins}m ago`;
27
+ if (diffHours < 24) return `${diffHours}h ago`;
28
+ if (diffDays < 7) return `${diffDays}d ago`;
29
+
30
+ return date.toLocaleDateString();
31
+ } catch (e) {
32
+ return '';
33
+ }
34
+ }
35
+
36
+ export function parseMarkdown(text, enhancedParser = null) {
37
+ if (enhancedParser) {
38
+ return enhancedParser(text);
39
+ }
40
+ let html = escapeHtml(text);
41
+ html = html.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>');
42
+ html = html.replace(/__(.+?)__/g, '<strong>$1</strong>');
43
+ html = html.replace(/\*(.+?)\*/g, '<em>$1</em>');
44
+ html = html.replace(/_(.+?)_/g, '<em>$1</em>');
45
+ html = html.replace(/`(.+?)`/g, '<code>$1</code>');
46
+ html = html.replace(/\[(.+?)\]\((.+?)\)/g, '<a href="$2" target="_blank" rel="noopener noreferrer">$1</a>');
47
+ html = html.replace(/\n/g, '<br>');
48
+ return html;
49
+ }
50
+
51
+ // Storage helpers
52
+ export function createStorage(prefix = '') {
53
+ const storageKey = (key) => prefix ? `${key}_${prefix}` : key;
54
+
55
+ return {
56
+ get(key) {
57
+ try { return localStorage.getItem(storageKey(key)); } catch (e) { return null; }
58
+ },
59
+ set(key, value) {
60
+ try {
61
+ const k = storageKey(key);
62
+ value === null ? localStorage.removeItem(k) : localStorage.setItem(k, value);
63
+ } catch (e) {}
64
+ }
65
+ };
66
+ }
67
+
68
+ // CSRF token helper
69
+ export function getCSRFToken(cookieName = 'csrftoken') {
70
+ const cookies = document.cookie.split(';');
71
+ for (let cookie of cookies) {
72
+ const [name, value] = cookie.trim().split('=');
73
+ if (name === cookieName) {
74
+ return decodeURIComponent(value);
75
+ }
76
+ }
77
+ const metaTag = document.querySelector('meta[name="csrf-token"]');
78
+ if (metaTag) {
79
+ return metaTag.getAttribute('content');
80
+ }
81
+ return null;
82
+ }
83
+