@makemore/agent-frontend 2.8.1 → 2.9.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.8.1",
3
+ "version": "2.9.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",
@@ -30,7 +30,7 @@
30
30
  "build:dev": "node build.js --dev",
31
31
  "build:embed": "esbuild src/index.js --bundle --minify --format=iife --global-name=ChatWidgetModule --outfile=dist/chat-widget.js",
32
32
  "watch": "node watch.js",
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'",
33
+ "copy": "mkdir -p ../django_agent_studio/static/agent-frontend && cp -f dist/chat-widget.js dist/chat-widget.css dist/chat-widget-markdown.js ../django_agent_studio/static/agent-frontend/ 2>/dev/null || true && echo 'Copied to django_agent_studio'",
34
34
  "prepublishOnly": "npm run build",
35
35
  "serve": "python -m http.server 8080"
36
36
  },
@@ -10,13 +10,16 @@ import { InputForm } from './InputForm.js';
10
10
  import { Sidebar } from './Sidebar.js';
11
11
  import { ModelSelector } from './ModelSelector.js';
12
12
  import { TaskList } from './TaskList.js';
13
+ import { DevToolbar } from './DevToolbar.js';
13
14
  import { useChat } from '../hooks/useChat.js';
14
15
  import { useModels } from '../hooks/useModels.js';
15
16
  import { useTasks } from '../hooks/useTasks.js';
17
+ import { useSystems } from '../hooks/useSystems.js';
16
18
  import { createApiClient } from '../utils/api.js';
17
- import { createStorage } from '../utils/helpers.js';
19
+ import { createStorage, getContrastingTextColor } from '../utils/helpers.js';
18
20
 
19
21
  export function ChatWidget({ config, onStateChange, markdownParser, apiRef }) {
22
+ console.log('[ChatWidget] Config:', { showConversationSidebar: config.showConversationSidebar, apiPaths: config.apiPaths });
20
23
  // UI state
21
24
  const [isOpen, setIsOpen] = useState(config.embedded || config.forceOpen === true);
22
25
  const [isExpanded, setIsExpanded] = useState(false);
@@ -54,8 +57,21 @@ export function ChatWidget({ config, onStateChange, markdownParser, apiRef }) {
54
57
  return createApiClient(config, getState, setState);
55
58
  }, [config, authToken, storage]);
56
59
 
57
- // Chat hook
58
- const chat = useChat(config, api, storage);
60
+ // Systems/agents discovery hook (for dev tools)
61
+ const systemsHook = useSystems(config, api, storage);
62
+
63
+ // Create a config override with the effective agent key from dev tools
64
+ const effectiveConfig = useMemo(() => {
65
+ if (!config.showDevTools) return config;
66
+ const effectiveKey = systemsHook.getEffectiveAgentKey();
67
+ if (effectiveKey && effectiveKey !== config.agentKey) {
68
+ return { ...config, agentKey: effectiveKey };
69
+ }
70
+ return config;
71
+ }, [config, systemsHook.getEffectiveAgentKey]);
72
+
73
+ // Chat hook — uses effective config so agentKey is dynamic
74
+ const chat = useChat(effectiveConfig, api, storage);
59
75
 
60
76
  // Models hook
61
77
  const models = useModels(config, api, storage);
@@ -86,7 +102,10 @@ export function ChatWidget({ config, onStateChange, markdownParser, apiRef }) {
86
102
  // Load initial conversation if stored
87
103
  useEffect(() => {
88
104
  const storedConvId = storage.get(config.conversationIdKey);
105
+ console.log('[ChatWidget] Initial load - storedConvId:', storedConvId, 'key:', config.conversationIdKey);
106
+ console.log('[ChatWidget] apiPaths.conversations:', config.apiPaths.conversations);
89
107
  if (storedConvId) {
108
+ console.log('[ChatWidget] Loading conversation:', storedConvId);
90
109
  chat.loadConversation(storedConvId);
91
110
  }
92
111
  }, []);
@@ -109,12 +128,13 @@ export function ChatWidget({ config, onStateChange, markdownParser, apiRef }) {
109
128
  // Load conversations for sidebar
110
129
  const loadConversations = useCallback(async () => {
111
130
  if (!config.showConversationSidebar) return;
112
-
131
+
113
132
  setConversationsLoading(true);
114
133
  try {
115
- const url = `${config.backendUrl}${config.apiPaths.conversations}?agent_key=${encodeURIComponent(config.agentKey)}`;
134
+ const agentKey = effectiveConfig.agentKey;
135
+ const url = `${config.backendUrl}${config.apiPaths.conversations}?agent_key=${encodeURIComponent(agentKey)}`;
116
136
  const response = await fetch(url, api.getFetchOptions({ method: 'GET' }));
117
-
137
+
118
138
  if (response.ok) {
119
139
  const data = await response.json();
120
140
  setConversations(data.results || data);
@@ -125,7 +145,7 @@ export function ChatWidget({ config, onStateChange, markdownParser, apiRef }) {
125
145
  } finally {
126
146
  setConversationsLoading(false);
127
147
  }
128
- }, [config, api]);
148
+ }, [config, effectiveConfig, api]);
129
149
 
130
150
  // Handlers
131
151
  const handleToggleSidebar = useCallback(() => {
@@ -149,6 +169,7 @@ export function ChatWidget({ config, onStateChange, markdownParser, apiRef }) {
149
169
  const handleSend = useCallback((content) => {
150
170
  chat.sendMessage(content, {
151
171
  model: models.selectedModel,
172
+ thinking: models.thinkingEnabled && models.supportsThinking(),
152
173
  onAssistantMessage: (assistantContent) => {
153
174
  // TTS callback when assistant finishes
154
175
  if (enableTTS && assistantContent) {
@@ -156,7 +177,7 @@ export function ChatWidget({ config, onStateChange, markdownParser, apiRef }) {
156
177
  }
157
178
  }
158
179
  });
159
- }, [chat, enableTTS, models.selectedModel]);
180
+ }, [chat, enableTTS, models.selectedModel, models.thinkingEnabled, models.supportsThinking]);
160
181
 
161
182
  // Handle tab switching
162
183
  const handleTabChange = useCallback((tab) => {
@@ -205,8 +226,11 @@ export function ChatWidget({ config, onStateChange, markdownParser, apiRef }) {
205
226
  config.embedded && 'cw-widget-embedded',
206
227
  ].filter(Boolean).join(' ');
207
228
 
229
+ // Calculate header text color for contrast
230
+ const headerTextColor = config.headerTextColor || getContrastingTextColor(config.primaryColor);
231
+
208
232
  return html`
209
- <div class=${widgetClasses} style=${{ '--cw-primary': config.primaryColor }}>
233
+ <div class=${widgetClasses} style=${{ '--cw-primary': config.primaryColor, '--cw-header-text': headerTextColor }}>
210
234
  ${config.showConversationSidebar && html`
211
235
  <${Sidebar}
212
236
  isOpen=${sidebarOpen}
@@ -235,6 +259,22 @@ export function ChatWidget({ config, onStateChange, markdownParser, apiRef }) {
235
259
  onToggleSidebar=${handleToggleSidebar}
236
260
  />
237
261
 
262
+ ${config.showDevTools && html`
263
+ <${DevToolbar}
264
+ systems=${systemsHook.systems}
265
+ agents=${systemsHook.agents}
266
+ selectedSystem=${systemsHook.selectedSystem}
267
+ selectedAgent=${systemsHook.selectedAgent}
268
+ selectedSystemVersion=${systemsHook.selectedSystemVersion}
269
+ selectedAgentVersion=${systemsHook.selectedAgentVersion}
270
+ onSelectSystem=${systemsHook.selectSystem}
271
+ onSelectAgent=${systemsHook.selectAgent}
272
+ onSelectSystemVersion=${systemsHook.selectSystemVersion}
273
+ onSelectAgentVersion=${systemsHook.selectAgentVersion}
274
+ disabled=${chat.isLoading}
275
+ />
276
+ `}
277
+
238
278
  ${config.showTasksTab !== false && html`
239
279
  <div class="cw-tabs">
240
280
  <button
@@ -276,6 +316,8 @@ export function ChatWidget({ config, onStateChange, markdownParser, apiRef }) {
276
316
  availableModels=${models.availableModels}
277
317
  selectedModel=${models.selectedModel}
278
318
  onSelectModel=${models.selectModel}
319
+ thinkingEnabled=${models.thinkingEnabled}
320
+ onToggleThinking=${models.toggleThinking}
279
321
  disabled=${chat.isLoading}
280
322
  />
281
323
  `}
@@ -0,0 +1,135 @@
1
+ /**
2
+ * DevToolbar component - system/agent/version picker for developers and testers.
3
+ *
4
+ * Always shows when rendered — displays info badges for single items,
5
+ * dropdowns when there are multiple options to choose from.
6
+ */
7
+
8
+ import { html } from 'htm/preact';
9
+ import { escapeHtml } from '../utils/helpers.js';
10
+
11
+ export function DevToolbar({
12
+ systems,
13
+ agents,
14
+ selectedSystem,
15
+ selectedAgent,
16
+ selectedSystemVersion,
17
+ selectedAgentVersion,
18
+ onSelectSystem,
19
+ onSelectAgent,
20
+ onSelectSystemVersion,
21
+ onSelectAgentVersion,
22
+ disabled,
23
+ }) {
24
+ // Find current system and agent objects
25
+ const currentSystem = systems.find(s => s.slug === selectedSystem);
26
+ const currentAgent = agents.find(a => a.slug === selectedAgent);
27
+
28
+ // Get versions for current selections
29
+ const systemVersions = currentSystem?.versions || [];
30
+ const agentVersions = currentAgent?.versions || [];
31
+
32
+ // Nothing loaded yet
33
+ if (systems.length === 0 && agents.length === 0) return null;
34
+
35
+ const hasMultipleSystems = systems.length > 1;
36
+ const hasMultipleAgents = agents.length > 1;
37
+ const hasMultipleSystemVersions = systemVersions.length > 1;
38
+ const hasMultipleAgentVersions = agentVersions.length > 1;
39
+
40
+ return html`
41
+ <div class="cw-dev-toolbar">
42
+ <div class="cw-dev-toolbar-label">
43
+ <span class="cw-dev-toolbar-icon">🛠️</span>
44
+ <span>Dev</span>
45
+ </div>
46
+
47
+ <div class="cw-dev-toolbar-selectors">
48
+ <!-- System -->
49
+ <div class="cw-dev-select-group">
50
+ <label class="cw-dev-label">System</label>
51
+ ${hasMultipleSystems ? html`
52
+ <select
53
+ class="cw-dev-select"
54
+ value=${selectedSystem || ''}
55
+ onChange=${(e) => onSelectSystem(e.target.value)}
56
+ disabled=${disabled}
57
+ >
58
+ ${systems.map(sys => html`
59
+ <option key=${sys.slug} value=${sys.slug}>
60
+ ${sys.name}
61
+ </option>
62
+ `)}
63
+ </select>
64
+ ` : html`
65
+ <span class="cw-dev-badge">${currentSystem?.name || '—'}</span>
66
+ `}
67
+ </div>
68
+
69
+ <!-- System Version -->
70
+ <div class="cw-dev-select-group">
71
+ <label class="cw-dev-label">Sys Ver</label>
72
+ ${hasMultipleSystemVersions ? html`
73
+ <select
74
+ class="cw-dev-select"
75
+ value=${selectedSystemVersion || ''}
76
+ onChange=${(e) => onSelectSystemVersion(e.target.value || null)}
77
+ disabled=${disabled}
78
+ >
79
+ ${systemVersions.map(v => html`
80
+ <option key=${v.version} value=${v.version}>
81
+ ${v.version}${v.is_active ? ' ✓' : ''}${v.is_draft ? ' (draft)' : ''}
82
+ </option>
83
+ `)}
84
+ </select>
85
+ ` : html`
86
+ <span class="cw-dev-badge">${systemVersions.length === 1 ? systemVersions[0].version : 'none'}</span>
87
+ `}
88
+ </div>
89
+
90
+ <!-- Agent -->
91
+ <div class="cw-dev-select-group">
92
+ <label class="cw-dev-label">Agent</label>
93
+ ${hasMultipleAgents ? html`
94
+ <select
95
+ class="cw-dev-select"
96
+ value=${selectedAgent || ''}
97
+ onChange=${(e) => onSelectAgent(e.target.value)}
98
+ disabled=${disabled}
99
+ >
100
+ ${agents.map(agent => html`
101
+ <option key=${agent.slug} value=${agent.slug}>
102
+ ${agent.name}${currentSystem?.entry_agent?.slug === agent.slug ? ' ★' : ''}
103
+ </option>
104
+ `)}
105
+ </select>
106
+ ` : html`
107
+ <span class="cw-dev-badge">${currentAgent?.name || agents[0]?.name || '—'}${currentSystem?.entry_agent?.slug === (currentAgent?.slug || agents[0]?.slug) ? ' ★' : ''}</span>
108
+ `}
109
+ </div>
110
+
111
+ <!-- Agent Version -->
112
+ <div class="cw-dev-select-group">
113
+ <label class="cw-dev-label">Agent Ver</label>
114
+ ${hasMultipleAgentVersions ? html`
115
+ <select
116
+ class="cw-dev-select"
117
+ value=${selectedAgentVersion || ''}
118
+ onChange=${(e) => onSelectAgentVersion(e.target.value || null)}
119
+ disabled=${disabled}
120
+ >
121
+ ${agentVersions.map(v => html`
122
+ <option key=${v.version} value=${v.version}>
123
+ ${v.version}${v.is_active ? ' ✓' : ''}${v.is_draft ? ' (draft)' : ''}
124
+ </option>
125
+ `)}
126
+ </select>
127
+ ` : html`
128
+ <span class="cw-dev-badge">${agentVersions.length === 1 ? agentVersions[0].version : (currentAgent?.active_version || '—')}</span>
129
+ `}
130
+ </div>
131
+ </div>
132
+ </div>
133
+ `;
134
+ }
135
+
@@ -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