@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.
package/package.json CHANGED
@@ -1,18 +1,24 @@
1
1
  {
2
2
  "name": "@makemore/agent-frontend",
3
- "version": "1.8.0",
4
- "description": "A standalone, zero-dependency chat widget for AI agents. Embed conversational AI into any website with a single script tag.",
3
+ "version": "2.0.0",
4
+ "description": "A lightweight chat widget for AI agents built with Preact. Embed conversational AI into any website with a single script tag.",
5
+ "type": "module",
5
6
  "main": "dist/chat-widget.js",
7
+ "module": "src/index.js",
6
8
  "files": [
7
9
  "dist/chat-widget.js",
8
10
  "dist/chat-widget.css",
9
11
  "dist/chat-widget-markdown.js",
12
+ "src/",
10
13
  "README.md",
11
14
  "LICENSE"
12
15
  ],
13
16
  "scripts": {
14
- "prepublishOnly": "npm run validate",
15
- "validate": "node -e \"console.log('Validation passed')\"",
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",
19
+ "watch": "node watch.js",
20
+ "copy": "cp dist/chat-widget.js ../django_agent_studio/static/agent-frontend/chat-widget.js && echo 'Copied to django_agent_studio'",
21
+ "prepublishOnly": "npm run build",
16
22
  "serve": "python -m http.server 8080"
17
23
  },
18
24
  "repository": {
@@ -27,8 +33,7 @@
27
33
  "agent",
28
34
  "sse",
29
35
  "streaming",
30
- "vanilla-js",
31
- "zero-dependencies",
36
+ "preact",
32
37
  "embeddable",
33
38
  "chat-widget",
34
39
  "conversational-ai",
@@ -41,7 +46,14 @@
41
46
  },
42
47
  "homepage": "https://github.com/makemore/agent-frontend#readme",
43
48
  "engines": {
44
- "node": ">=12.0.0"
49
+ "node": ">=14.0.0"
50
+ },
51
+ "dependencies": {
52
+ "htm": "^3.1.1",
53
+ "preact": "^10.19.3"
54
+ },
55
+ "devDependencies": {
56
+ "esbuild": "^0.20.0"
45
57
  }
46
58
  }
47
59
 
@@ -0,0 +1,259 @@
1
+ /**
2
+ * ChatWidget component - main widget container
3
+ */
4
+
5
+ import { html } from 'htm/preact';
6
+ import { useState, useEffect, useCallback, useMemo } from 'preact/hooks';
7
+ import { Header } from './Header.js';
8
+ import { MessageList } from './MessageList.js';
9
+ import { InputForm } from './InputForm.js';
10
+ import { Sidebar } from './Sidebar.js';
11
+ import { ModelSelector } from './ModelSelector.js';
12
+ import { useChat } from '../hooks/useChat.js';
13
+ import { useModels } from '../hooks/useModels.js';
14
+ import { createApiClient } from '../utils/api.js';
15
+ import { createStorage } from '../utils/helpers.js';
16
+
17
+ export function ChatWidget({ config, onStateChange, markdownParser, apiRef }) {
18
+ // UI state
19
+ const [isOpen, setIsOpen] = useState(config.embedded || config.forceOpen === true);
20
+ const [isExpanded, setIsExpanded] = useState(false);
21
+ const [debugMode, setDebugMode] = useState(false);
22
+ const [sidebarOpen, setSidebarOpen] = useState(false);
23
+ const [conversations, setConversations] = useState([]);
24
+ const [conversationsLoading, setConversationsLoading] = useState(false);
25
+
26
+ // TTS state
27
+ const [enableTTS, setEnableTTS] = useState(config.enableTTS);
28
+ const [isSpeaking, setIsSpeaking] = useState(false);
29
+
30
+ // Current agent state (for multi-agent systems)
31
+ const [currentAgent, setCurrentAgent] = useState(null);
32
+
33
+ // Handle forceOpen changes from parent
34
+ useEffect(() => {
35
+ if (config.forceOpen !== undefined) {
36
+ setIsOpen(config.forceOpen);
37
+ }
38
+ }, [config.forceOpen]);
39
+
40
+ // Create storage helper
41
+ const storage = useMemo(() => createStorage(config.containerId), [config.containerId]);
42
+
43
+ // Create API client
44
+ const [authToken, setAuthToken] = useState(config.authToken || null);
45
+ const api = useMemo(() => {
46
+ const getState = () => ({ authToken, storage });
47
+ const setState = (fn) => {
48
+ const newState = fn({ authToken, storage });
49
+ if (newState.authToken !== authToken) setAuthToken(newState.authToken);
50
+ };
51
+ return createApiClient(config, getState, setState);
52
+ }, [config, authToken, storage]);
53
+
54
+ // Chat hook
55
+ const chat = useChat(config, api, storage);
56
+
57
+ // Models hook
58
+ const models = useModels(config, api, storage);
59
+
60
+ // Track current agent from messages (for multi-agent systems)
61
+ useEffect(() => {
62
+ // Look for the most recent sub_agent_start or sub_agent_end message
63
+ for (let i = chat.messages.length - 1; i >= 0; i--) {
64
+ const msg = chat.messages[i];
65
+ if (msg.type === 'sub_agent_start') {
66
+ setCurrentAgent({
67
+ key: msg.metadata?.subAgentKey,
68
+ name: msg.metadata?.agentName,
69
+ });
70
+ return;
71
+ }
72
+ if (msg.type === 'sub_agent_end') {
73
+ // Sub-agent finished, clear the current agent indicator
74
+ setCurrentAgent(null);
75
+ return;
76
+ }
77
+ }
78
+ }, [chat.messages]);
79
+
80
+ // Load initial conversation if stored
81
+ useEffect(() => {
82
+ const storedConvId = storage.get(config.conversationIdKey);
83
+ if (storedConvId) {
84
+ chat.loadConversation(storedConvId);
85
+ }
86
+ }, []);
87
+
88
+ // Notify parent of state changes
89
+ useEffect(() => {
90
+ if (onStateChange) {
91
+ onStateChange({
92
+ isOpen,
93
+ isExpanded,
94
+ debugMode,
95
+ messages: chat.messages,
96
+ conversationId: chat.conversationId,
97
+ isLoading: chat.isLoading,
98
+ error: chat.error,
99
+ });
100
+ }
101
+ }, [isOpen, isExpanded, debugMode, chat.messages, chat.conversationId, chat.isLoading, chat.error]);
102
+
103
+ // Load conversations for sidebar
104
+ const loadConversations = useCallback(async () => {
105
+ if (!config.showConversationSidebar) return;
106
+
107
+ setConversationsLoading(true);
108
+ try {
109
+ const url = `${config.backendUrl}${config.apiPaths.conversations}?agent_key=${encodeURIComponent(config.agentKey)}`;
110
+ const response = await fetch(url, api.getFetchOptions({ method: 'GET' }));
111
+
112
+ if (response.ok) {
113
+ const data = await response.json();
114
+ setConversations(data.results || data);
115
+ }
116
+ } catch (err) {
117
+ console.error('[ChatWidget] Failed to load conversations:', err);
118
+ setConversations([]);
119
+ } finally {
120
+ setConversationsLoading(false);
121
+ }
122
+ }, [config, api]);
123
+
124
+ // Handlers
125
+ const handleToggleSidebar = useCallback(() => {
126
+ const newOpen = !sidebarOpen;
127
+ setSidebarOpen(newOpen);
128
+ if (newOpen) loadConversations();
129
+ }, [sidebarOpen, loadConversations]);
130
+
131
+ const handleSwitchConversation = useCallback((convId) => {
132
+ if (convId !== chat.conversationId) {
133
+ chat.loadConversation(convId);
134
+ }
135
+ setSidebarOpen(false);
136
+ }, [chat]);
137
+
138
+ const handleNewConversation = useCallback(() => {
139
+ chat.clearMessages();
140
+ setSidebarOpen(false);
141
+ }, [chat]);
142
+
143
+ const handleSend = useCallback((content) => {
144
+ chat.sendMessage(content, {
145
+ model: models.selectedModel,
146
+ onAssistantMessage: (assistantContent) => {
147
+ // TTS callback when assistant finishes
148
+ if (enableTTS && assistantContent) {
149
+ // TTS would be handled here
150
+ }
151
+ }
152
+ });
153
+ }, [chat, enableTTS, models.selectedModel]);
154
+
155
+ // Expose imperative API to parent
156
+ useEffect(() => {
157
+ if (apiRef) {
158
+ apiRef.current = {
159
+ open: () => setIsOpen(true),
160
+ close: () => setIsOpen(false),
161
+ send: (msg) => handleSend(msg),
162
+ clearMessages: () => chat.clearMessages(),
163
+ toggleTTS: () => setEnableTTS(v => !v),
164
+ stopSpeech: () => setIsSpeaking(false),
165
+ setAuth: (cfg) => {
166
+ if (cfg.token !== undefined) setAuthToken(cfg.token);
167
+ },
168
+ clearAuth: () => setAuthToken(null),
169
+ };
170
+ }
171
+ }, [chat, apiRef, handleSend]);
172
+
173
+ // Floating mode: show FAB when closed
174
+ if (!config.embedded && !isOpen) {
175
+ return html`
176
+ <button
177
+ class="cw-fab"
178
+ style=${{ backgroundColor: config.primaryColor }}
179
+ onClick=${() => setIsOpen(true)}
180
+ >
181
+ <svg class="cw-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
182
+ <path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"></path>
183
+ </svg>
184
+ </button>
185
+ `;
186
+ }
187
+
188
+ const widgetClasses = [
189
+ 'cw-widget',
190
+ isExpanded && 'cw-widget-expanded',
191
+ config.embedded && 'cw-widget-embedded',
192
+ ].filter(Boolean).join(' ');
193
+
194
+ return html`
195
+ <div class=${widgetClasses} style=${{ '--cw-primary': config.primaryColor }}>
196
+ ${config.showConversationSidebar && html`
197
+ <${Sidebar}
198
+ isOpen=${sidebarOpen}
199
+ conversations=${conversations}
200
+ conversationsLoading=${conversationsLoading}
201
+ currentConversationId=${chat.conversationId}
202
+ onClose=${() => setSidebarOpen(false)}
203
+ onNewConversation=${handleNewConversation}
204
+ onSwitchConversation=${handleSwitchConversation}
205
+ />
206
+ `}
207
+
208
+ <${Header}
209
+ config=${config}
210
+ debugMode=${debugMode}
211
+ isExpanded=${isExpanded}
212
+ isSpeaking=${isSpeaking}
213
+ messagesCount=${chat.messages.length}
214
+ isLoading=${chat.isLoading}
215
+ currentAgent=${currentAgent}
216
+ onClose=${() => setIsOpen(false)}
217
+ onToggleExpand=${() => setIsExpanded(!isExpanded)}
218
+ onToggleDebug=${() => setDebugMode(!debugMode)}
219
+ onToggleTTS=${() => setEnableTTS(!enableTTS)}
220
+ onClear=${chat.clearMessages}
221
+ onToggleSidebar=${handleToggleSidebar}
222
+ />
223
+
224
+ ${debugMode && html`<div class="cw-status-bar"><span>🐛 Debug</span></div>`}
225
+
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
+ />
237
+
238
+ ${chat.error && html`<div class="cw-error-bar">${chat.error}</div>`}
239
+
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}
246
+ />
247
+ `}
248
+
249
+ <${InputForm}
250
+ onSend=${handleSend}
251
+ onCancel=${chat.cancelRun}
252
+ isLoading=${chat.isLoading}
253
+ placeholder=${config.placeholder}
254
+ primaryColor=${config.primaryColor}
255
+ />
256
+ </div>
257
+ `;
258
+ }
259
+
@@ -0,0 +1,111 @@
1
+ /**
2
+ * Header component - title bar with action buttons
3
+ */
4
+
5
+ import { html } from 'htm/preact';
6
+ import { escapeHtml } from '../utils/helpers.js';
7
+
8
+ export function Header({
9
+ config,
10
+ debugMode,
11
+ isExpanded,
12
+ isSpeaking,
13
+ messagesCount,
14
+ isLoading,
15
+ currentAgent,
16
+ onClose,
17
+ onToggleExpand,
18
+ onToggleDebug,
19
+ onToggleTTS,
20
+ onClear,
21
+ onToggleSidebar,
22
+ }) {
23
+ const {
24
+ title,
25
+ primaryColor,
26
+ embedded,
27
+ showConversationSidebar,
28
+ showClearButton,
29
+ showDebugButton,
30
+ enableDebugMode,
31
+ showTTSButton,
32
+ showExpandButton,
33
+ enableTTS,
34
+ elevenLabsApiKey,
35
+ ttsProxyUrl,
36
+ } = config;
37
+
38
+ const hasTTS = elevenLabsApiKey || ttsProxyUrl;
39
+
40
+ return html`
41
+ <div class="cw-header" style=${{ backgroundColor: primaryColor }}>
42
+ ${showConversationSidebar && html`
43
+ <button
44
+ class="cw-header-btn cw-hamburger"
45
+ onClick=${onToggleSidebar}
46
+ title="Conversations"
47
+ >
48
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="16" height="16">
49
+ <line x1="3" y1="6" x2="21" y2="6"></line>
50
+ <line x1="3" y1="12" x2="21" y2="12"></line>
51
+ <line x1="3" y1="18" x2="21" y2="18"></line>
52
+ </svg>
53
+ </button>
54
+ `}
55
+
56
+ <div class="cw-title-container">
57
+ <span class="cw-title">${escapeHtml(title)}</span>
58
+ ${currentAgent && html`
59
+ <span class="cw-current-agent" title="Currently active agent">
60
+ <span class="cw-agent-indicator">🤖</span>
61
+ <span class="cw-agent-name">${escapeHtml(currentAgent.name || currentAgent.key)}</span>
62
+ </span>
63
+ `}
64
+ </div>
65
+
66
+ <div class="cw-header-actions">
67
+ ${showClearButton && html`
68
+ <button
69
+ class="cw-header-btn"
70
+ onClick=${onClear}
71
+ title="Clear"
72
+ disabled=${isLoading || messagesCount === 0}
73
+ >🗑️</button>
74
+ `}
75
+
76
+ ${showDebugButton && enableDebugMode && html`
77
+ <button
78
+ class="cw-header-btn ${debugMode ? 'cw-btn-active' : ''}"
79
+ onClick=${onToggleDebug}
80
+ title="Debug"
81
+ >🐛</button>
82
+ `}
83
+
84
+ ${showTTSButton && hasTTS && html`
85
+ <button
86
+ class="cw-header-btn ${enableTTS ? 'cw-btn-active' : ''}"
87
+ onClick=${onToggleTTS}
88
+ title="TTS"
89
+ >${enableTTS ? '🔊' : '🔇'}</button>
90
+ `}
91
+
92
+ ${showExpandButton && !embedded && html`
93
+ <button
94
+ class="cw-header-btn"
95
+ onClick=${onToggleExpand}
96
+ title=${isExpanded ? 'Minimize' : 'Expand'}
97
+ >${isExpanded ? '⊖' : '⊕'}</button>
98
+ `}
99
+
100
+ ${!embedded && html`
101
+ <button
102
+ class="cw-header-btn"
103
+ onClick=${onClose}
104
+ title="Close"
105
+ >✕</button>
106
+ `}
107
+ </div>
108
+ </div>
109
+ `;
110
+ }
111
+
@@ -0,0 +1,95 @@
1
+ /**
2
+ * InputForm component - message input and send button
3
+ * Supports multiline input with Shift+Enter for newlines, Enter to send
4
+ * Shows a stop button when loading that can cancel the current run
5
+ */
6
+
7
+ import { html } from 'htm/preact';
8
+ import { useState, useRef, useEffect } from 'preact/hooks';
9
+ import { escapeHtml } from '../utils/helpers.js';
10
+
11
+ export function InputForm({ onSend, onCancel, isLoading, placeholder, primaryColor }) {
12
+ const [value, setValue] = useState('');
13
+ const [isHovering, setIsHovering] = useState(false);
14
+ const textareaRef = useRef(null);
15
+
16
+ // Focus input when not loading
17
+ useEffect(() => {
18
+ if (!isLoading && textareaRef.current) {
19
+ textareaRef.current.focus();
20
+ }
21
+ }, [isLoading]);
22
+
23
+ // Auto-resize textarea based on content
24
+ useEffect(() => {
25
+ if (textareaRef.current) {
26
+ textareaRef.current.style.height = 'auto';
27
+ textareaRef.current.style.height = Math.min(textareaRef.current.scrollHeight, 150) + 'px';
28
+ }
29
+ }, [value]);
30
+
31
+ const handleSubmit = (e) => {
32
+ e.preventDefault();
33
+ if (value.trim() && !isLoading) {
34
+ onSend(value);
35
+ setValue('');
36
+ // Reset height after sending
37
+ if (textareaRef.current) {
38
+ textareaRef.current.style.height = 'auto';
39
+ }
40
+ }
41
+ };
42
+
43
+ const handleKeyDown = (e) => {
44
+ // Enter without Shift sends the message
45
+ if (e.key === 'Enter' && !e.shiftKey) {
46
+ e.preventDefault();
47
+ handleSubmit(e);
48
+ }
49
+ // Shift+Enter allows newline (default behavior)
50
+ };
51
+
52
+ const handleButtonClick = (e) => {
53
+ if (isLoading && onCancel) {
54
+ e.preventDefault();
55
+ onCancel();
56
+ }
57
+ };
58
+
59
+ // Stop icon (square)
60
+ const stopIcon = html`
61
+ <svg width="14" height="14" viewBox="0 0 14 14" fill="currentColor">
62
+ <rect x="2" y="2" width="10" height="10" rx="1" />
63
+ </svg>
64
+ `;
65
+
66
+ return html`
67
+ <form class="cw-input-form" onSubmit=${handleSubmit}>
68
+ <textarea
69
+ ref=${textareaRef}
70
+ class="cw-input"
71
+ placeholder=${escapeHtml(placeholder)}
72
+ value=${value}
73
+ onInput=${e => setValue(e.target.value)}
74
+ onKeyDown=${handleKeyDown}
75
+ disabled=${isLoading}
76
+ rows="1"
77
+ />
78
+ <button
79
+ type=${isLoading ? 'button' : 'submit'}
80
+ class=${`cw-send-btn ${isLoading ? 'cw-send-btn-loading' : ''} ${isLoading && isHovering ? 'cw-send-btn-stop' : ''}`}
81
+ style=${{ backgroundColor: isLoading && isHovering ? '#dc2626' : primaryColor }}
82
+ onClick=${handleButtonClick}
83
+ onMouseEnter=${() => setIsHovering(true)}
84
+ onMouseLeave=${() => setIsHovering(false)}
85
+ title=${isLoading ? 'Stop' : 'Send'}
86
+ >
87
+ ${isLoading
88
+ ? (isHovering ? stopIcon : html`<span class="cw-spinner"></span>`)
89
+ : '➤'
90
+ }
91
+ </button>
92
+ </form>
93
+ `;
94
+ }
95
+
@@ -0,0 +1,115 @@
1
+ /**
2
+ * Message component - renders a single chat message
3
+ */
4
+
5
+ import { html } from 'htm/preact';
6
+ import { useState } from 'preact/hooks';
7
+ import { escapeHtml, parseMarkdown } from '../utils/helpers.js';
8
+
9
+ // Debug payload viewer component
10
+ function DebugPayload({ msg, show, onToggle }) {
11
+ if (!show) {
12
+ return html`
13
+ <button
14
+ class="cw-debug-payload-btn"
15
+ onClick=${onToggle}
16
+ title="Show message payload"
17
+ >{ }</button>
18
+ `;
19
+ }
20
+
21
+ return html`
22
+ <div class="cw-debug-payload">
23
+ <button class="cw-debug-payload-close" onClick=${onToggle}>×</button>
24
+ <pre class="cw-debug-payload-content">${JSON.stringify(msg, null, 2)}</pre>
25
+ </div>
26
+ `;
27
+ }
28
+
29
+ export function Message({ msg, debugMode, markdownParser }) {
30
+ const [expanded, setExpanded] = useState(false);
31
+ const [showPayload, setShowPayload] = useState(false);
32
+
33
+ const isUser = msg.role === 'user';
34
+ const isSystem = msg.role === 'system';
35
+ const isToolCall = msg.type === 'tool_call';
36
+ const isToolResult = msg.type === 'tool_result';
37
+ const isError = msg.type === 'error';
38
+ const isSubAgentStart = msg.type === 'sub_agent_start';
39
+ const isSubAgentEnd = msg.type === 'sub_agent_end';
40
+ const isAgentContext = msg.type === 'agent_context';
41
+
42
+ // Hide system messages unless debug mode is on
43
+ if (isSystem && !debugMode) {
44
+ return null;
45
+ }
46
+
47
+ // Sub-agent delegation messages - show as a distinct visual element
48
+ if (isSubAgentStart || isSubAgentEnd || isAgentContext) {
49
+ return html`
50
+ <div class="cw-agent-context ${isSubAgentStart ? 'cw-agent-delegating' : ''} ${isSubAgentEnd ? 'cw-agent-returned' : ''}" style="position: relative;">
51
+ <span class="cw-agent-context-icon">${isSubAgentStart ? '🔗' : isSubAgentEnd ? '✓' : '🤖'}</span>
52
+ <span class="cw-agent-context-text">${msg.content}</span>
53
+ ${msg.metadata?.agentName && html`
54
+ <span class="cw-agent-context-name">${msg.metadata.agentName}</span>
55
+ `}
56
+ ${debugMode && html`<${DebugPayload} msg=${msg} show=${showPayload} onToggle=${() => setShowPayload(!showPayload)} />`}
57
+ </div>
58
+ `;
59
+ }
60
+
61
+ // Tool call/result: show compact inline version
62
+ if (isToolCall || isToolResult) {
63
+ const hasDetails = msg.metadata?.arguments || msg.metadata?.result;
64
+
65
+ // Format details - handle case where data is already a JSON string
66
+ const formatDetails = (data) => {
67
+ if (typeof data === 'string') {
68
+ try {
69
+ // Try to parse and re-stringify for proper formatting
70
+ return JSON.stringify(JSON.parse(data), null, 2);
71
+ } catch {
72
+ // Not valid JSON, return as-is
73
+ return data;
74
+ }
75
+ }
76
+ return JSON.stringify(data, null, 2);
77
+ };
78
+
79
+ return html`
80
+ <div class="cw-tool-message ${isToolResult ? 'cw-tool-result' : 'cw-tool-call'}" style="position: relative;">
81
+ <span class="cw-tool-label" onClick=${() => hasDetails && setExpanded(!expanded)}>
82
+ ${msg.content}
83
+ ${hasDetails && html`<span class="cw-tool-expand">${expanded ? '▼' : '▶'}</span>`}
84
+ </span>
85
+ ${expanded && hasDetails && html`
86
+ <pre class="cw-tool-details">${escapeHtml(formatDetails(
87
+ isToolCall ? msg.metadata.arguments : msg.metadata.result
88
+ ))}</pre>
89
+ `}
90
+ ${debugMode && html`<${DebugPayload} msg=${msg} show=${showPayload} onToggle=${() => setShowPayload(!showPayload)} />`}
91
+ </div>
92
+ `;
93
+ }
94
+
95
+ const classes = [
96
+ 'cw-message',
97
+ isUser && 'cw-message-user',
98
+ isError && 'cw-message-error',
99
+ ].filter(Boolean).join(' ');
100
+
101
+ const rowClasses = `cw-message-row ${isUser ? 'cw-message-row-user' : ''}`;
102
+
103
+ // Parse content
104
+ let content = msg.role === 'assistant'
105
+ ? parseMarkdown(msg.content, markdownParser)
106
+ : escapeHtml(msg.content);
107
+
108
+ return html`
109
+ <div class=${rowClasses} style="position: relative;">
110
+ <div class=${classes} dangerouslySetInnerHTML=${{ __html: content }} />
111
+ ${debugMode && html`<${DebugPayload} msg=${msg} show=${showPayload} onToggle=${() => setShowPayload(!showPayload)} />`}
112
+ </div>
113
+ `;
114
+ }
115
+