vanilla-agent 1.0.0 → 1.2.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/dist/widget.css CHANGED
@@ -783,3 +783,43 @@ form:focus-within textarea {
783
783
  .tvw-voice-recording svg {
784
784
  animation: voice-recording-pulse 1.5s ease-in-out infinite;
785
785
  }
786
+
787
+ /* Markdown content overflow handling */
788
+ #vanilla-agent-root pre {
789
+ overflow-x: auto;
790
+ max-width: 100%;
791
+ word-wrap: break-word;
792
+ word-break: break-word;
793
+ white-space: pre-wrap;
794
+ background-color: #f3f4f6;
795
+ padding: 0.75rem;
796
+ border-radius: 0.375rem;
797
+ margin: 0.5rem 0;
798
+ font-size: 0.875rem;
799
+ line-height: 1.5;
800
+ border: 1px solid #e5e7eb;
801
+ }
802
+
803
+ #vanilla-agent-root code {
804
+ word-break: break-word;
805
+ word-wrap: break-word;
806
+ white-space: pre-wrap;
807
+ overflow-wrap: break-word;
808
+ font-family: ui-monospace, SFMono-Regular, "SF Mono", Consolas, "Liberation Mono", Menlo, monospace;
809
+ font-size: 0.875em;
810
+ }
811
+
812
+ #vanilla-agent-root pre code {
813
+ font-size: inherit;
814
+ background-color: transparent;
815
+ padding: 0;
816
+ border-radius: 0;
817
+ }
818
+
819
+ #vanilla-agent-root img {
820
+ max-width: 100%;
821
+ height: auto;
822
+ display: block;
823
+ margin: 0.5rem 0;
824
+ border-radius: 0.375rem;
825
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vanilla-agent",
3
- "version": "1.0.0",
3
+ "version": "1.2.0",
4
4
  "description": "Themeable, plugable streaming agent widget for websites, in plain JS with support for voice input and reasoning / tool output.",
5
5
  "type": "module",
6
6
  "main": "dist/index.cjs",
@@ -7,6 +7,35 @@ export type MessageTransform = (context: {
7
7
  streaming: boolean;
8
8
  }) => string;
9
9
 
10
+ // Create typing indicator element
11
+ export const createTypingIndicator = (): HTMLElement => {
12
+ const container = document.createElement("div");
13
+ container.className = "tvw-flex tvw-items-center tvw-space-x-1 tvw-h-5 tvw-mt-2";
14
+
15
+ const dot1 = document.createElement("div");
16
+ dot1.className = "tvw-bg-cw-primary tvw-animate-typing tvw-rounded-full tvw-h-1.5 tvw-w-1.5";
17
+ dot1.style.animationDelay = "0ms";
18
+
19
+ const dot2 = document.createElement("div");
20
+ dot2.className = "tvw-bg-cw-primary tvw-animate-typing tvw-rounded-full tvw-h-1.5 tvw-w-1.5";
21
+ dot2.style.animationDelay = "250ms";
22
+
23
+ const dot3 = document.createElement("div");
24
+ dot3.className = "tvw-bg-cw-primary tvw-animate-typing tvw-rounded-full tvw-h-1.5 tvw-w-1.5";
25
+ dot3.style.animationDelay = "500ms";
26
+
27
+ const srOnly = document.createElement("span");
28
+ srOnly.className = "tvw-sr-only";
29
+ srOnly.textContent = "Loading";
30
+
31
+ container.appendChild(dot1);
32
+ container.appendChild(dot2);
33
+ container.appendChild(dot3);
34
+ container.appendChild(srOnly);
35
+
36
+ return container;
37
+ };
38
+
10
39
  export const createStandardBubble = (
11
40
  message: AgentWidgetMessage,
12
41
  transform: MessageTransform
@@ -39,11 +68,21 @@ export const createStandardBubble = (
39
68
  }
40
69
 
41
70
  const bubble = createElement("div", classes.join(" "));
42
- bubble.innerHTML = transform({
71
+
72
+ // Add message content
73
+ const contentDiv = document.createElement("div");
74
+ contentDiv.innerHTML = transform({
43
75
  text: message.content,
44
76
  message,
45
77
  streaming: Boolean(message.streaming)
46
78
  });
79
+ bubble.appendChild(contentDiv);
80
+
81
+ // Add typing indicator if this is a streaming assistant message with content
82
+ if (message.streaming && message.role === "assistant" && message.content && message.content.trim()) {
83
+ const typingIndicator = createTypingIndicator();
84
+ bubble.appendChild(typingIndicator);
85
+ }
47
86
 
48
87
  return bubble;
49
88
  };
@@ -116,7 +116,7 @@ export const initAgentWidget = (
116
116
  let controller = createAgentExperience(mount, options.config);
117
117
  options.onReady?.();
118
118
 
119
- return {
119
+ const handle: AgentWidgetInitHandle = {
120
120
  host,
121
121
  update(nextConfig: AgentWidgetConfig) {
122
122
  controller.update(nextConfig);
@@ -133,9 +133,32 @@ export const initAgentWidget = (
133
133
  clearChat() {
134
134
  controller.clearChat();
135
135
  },
136
+ setMessage(message: string) {
137
+ return controller.setMessage(message);
138
+ },
139
+ submitMessage(message?: string) {
140
+ return controller.submitMessage(message);
141
+ },
142
+ startVoiceRecognition() {
143
+ return controller.startVoiceRecognition();
144
+ },
145
+ stopVoiceRecognition() {
146
+ return controller.stopVoiceRecognition();
147
+ },
136
148
  destroy() {
137
149
  controller.destroy();
138
150
  host.remove();
151
+ // Clean up window reference if it was set
152
+ if (options.windowKey && typeof window !== 'undefined') {
153
+ delete (window as any)[options.windowKey];
154
+ }
139
155
  }
140
156
  };
157
+
158
+ // Store on window if windowKey is provided
159
+ if (options.windowKey && typeof window !== 'undefined') {
160
+ (window as any)[options.windowKey] = handle;
161
+ }
162
+
163
+ return handle;
141
164
  };
@@ -783,3 +783,43 @@ form:focus-within textarea {
783
783
  .tvw-voice-recording svg {
784
784
  animation: voice-recording-pulse 1.5s ease-in-out infinite;
785
785
  }
786
+
787
+ /* Markdown content overflow handling */
788
+ #vanilla-agent-root pre {
789
+ overflow-x: auto;
790
+ max-width: 100%;
791
+ word-wrap: break-word;
792
+ word-break: break-word;
793
+ white-space: pre-wrap;
794
+ background-color: #f3f4f6;
795
+ padding: 0.75rem;
796
+ border-radius: 0.375rem;
797
+ margin: 0.5rem 0;
798
+ font-size: 0.875rem;
799
+ line-height: 1.5;
800
+ border: 1px solid #e5e7eb;
801
+ }
802
+
803
+ #vanilla-agent-root code {
804
+ word-break: break-word;
805
+ word-wrap: break-word;
806
+ white-space: pre-wrap;
807
+ overflow-wrap: break-word;
808
+ font-family: ui-monospace, SFMono-Regular, "SF Mono", Consolas, "Liberation Mono", Menlo, monospace;
809
+ font-size: 0.875em;
810
+ }
811
+
812
+ #vanilla-agent-root pre code {
813
+ font-size: inherit;
814
+ background-color: transparent;
815
+ padding: 0;
816
+ border-radius: 0;
817
+ }
818
+
819
+ #vanilla-agent-root img {
820
+ max-width: 100%;
821
+ height: auto;
822
+ display: block;
823
+ margin: 0.5rem 0;
824
+ border-radius: 0.375rem;
825
+ }
package/src/types.ts CHANGED
@@ -216,4 +216,5 @@ export type AgentWidgetInitOptions = {
216
216
  config?: AgentWidgetConfig;
217
217
  useShadowDom?: boolean;
218
218
  onReady?: () => void;
219
+ windowKey?: string; // If provided, stores the controller on window[windowKey] for global access
219
220
  };
package/src/ui.ts CHANGED
@@ -8,7 +8,7 @@ import { statusCopy } from "./utils/constants";
8
8
  import { createLauncherButton } from "./components/launcher";
9
9
  import { createWrapper, buildPanel } from "./components/panel";
10
10
  import { MessageTransform } from "./components/message-bubble";
11
- import { createStandardBubble } from "./components/message-bubble";
11
+ import { createStandardBubble, createTypingIndicator } from "./components/message-bubble";
12
12
  import { createReasoningBubble } from "./components/reasoning-bubble";
13
13
  import { createToolBubble } from "./components/tool-bubble";
14
14
  import { createSuggestions } from "./components/suggestions";
@@ -23,6 +23,10 @@ type Controller = {
23
23
  close: () => void;
24
24
  toggle: () => void;
25
25
  clearChat: () => void;
26
+ setMessage: (message: string) => boolean;
27
+ submitMessage: (message?: string) => boolean;
28
+ startVoiceRecognition: () => boolean;
29
+ stopVoiceRecognition: () => boolean;
26
30
  };
27
31
 
28
32
  const buildPostprocessor = (cfg?: AgentWidgetConfig): MessageTransform => {
@@ -148,34 +152,6 @@ export const createAgentExperience = (
148
152
  });
149
153
  };
150
154
 
151
- // Create typing indicator element
152
- const createTypingIndicator = (): HTMLElement => {
153
- const container = document.createElement("div");
154
- container.className = "tvw-flex tvw-items-center tvw-space-x-1 tvw-h-5";
155
-
156
- const dot1 = document.createElement("div");
157
- dot1.className = "tvw-bg-cw-primary tvw-animate-typing tvw-rounded-full tvw-h-1.5 tvw-w-1.5";
158
- dot1.style.animationDelay = "0ms";
159
-
160
- const dot2 = document.createElement("div");
161
- dot2.className = "tvw-bg-cw-primary tvw-animate-typing tvw-rounded-full tvw-h-1.5 tvw-w-1.5";
162
- dot2.style.animationDelay = "250ms";
163
-
164
- const dot3 = document.createElement("div");
165
- dot3.className = "tvw-bg-cw-primary tvw-animate-typing tvw-rounded-full tvw-h-1.5 tvw-w-1.5";
166
- dot3.style.animationDelay = "500ms";
167
-
168
- const srOnly = document.createElement("span");
169
- srOnly.className = "tvw-sr-only";
170
- srOnly.textContent = "Loading";
171
-
172
- container.appendChild(dot1);
173
- container.appendChild(dot2);
174
- container.appendChild(dot3);
175
- container.appendChild(srOnly);
176
-
177
- return container;
178
- };
179
155
 
180
156
  // Message rendering with plugin support
181
157
  const renderMessagesWithPlugins = (
@@ -258,10 +234,15 @@ export const createAgentExperience = (
258
234
  fragment.appendChild(wrapper);
259
235
  });
260
236
 
261
- // Add typing indicator if streaming and there's at least one user message
262
- if (isStreaming && messages.some((msg) => msg.role === "user")) {
237
+ // Add standalone typing indicator only if streaming but no assistant message is streaming yet
238
+ // (This shows while waiting for the stream to start)
239
+ const hasStreamingAssistantMessage = messages.some(
240
+ (msg) => msg.role === "assistant" && msg.streaming && msg.content && msg.content.trim()
241
+ );
242
+
243
+ if (isStreaming && messages.some((msg) => msg.role === "user") && !hasStreamingAssistantMessage) {
263
244
  const typingIndicator = createTypingIndicator();
264
-
245
+
265
246
  // Create a bubble wrapper for the typing indicator (similar to assistant messages)
266
247
  const typingBubble = document.createElement("div");
267
248
  typingBubble.className = [
@@ -277,9 +258,9 @@ export const createAgentExperience = (
277
258
  "tvw-px-5",
278
259
  "tvw-py-3"
279
260
  ].join(" ");
280
-
261
+
281
262
  typingBubble.appendChild(typingIndicator);
282
-
263
+
283
264
  const typingWrapper = document.createElement("div");
284
265
  typingWrapper.className = "tvw-flex";
285
266
  typingWrapper.appendChild(typingBubble);
@@ -1619,6 +1600,55 @@ export const createAgentExperience = (
1619
1600
  });
1620
1601
  window.dispatchEvent(clearEvent);
1621
1602
  },
1603
+ setMessage(message: string): boolean {
1604
+ if (!textarea) return false;
1605
+ if (session.isStreaming()) return false;
1606
+
1607
+ // Auto-open widget if closed and launcher is enabled
1608
+ if (!open && launcherEnabled) {
1609
+ setOpenState(true);
1610
+ }
1611
+
1612
+ textarea.value = message;
1613
+ // Trigger input event for any listeners
1614
+ textarea.dispatchEvent(new Event('input', { bubbles: true }));
1615
+ return true;
1616
+ },
1617
+ submitMessage(message?: string): boolean {
1618
+ if (session.isStreaming()) return false;
1619
+
1620
+ const valueToSubmit = message?.trim() || textarea.value.trim();
1621
+ if (!valueToSubmit) return false;
1622
+
1623
+ // Auto-open widget if closed and launcher is enabled
1624
+ if (!open && launcherEnabled) {
1625
+ setOpenState(true);
1626
+ }
1627
+
1628
+ textarea.value = "";
1629
+ session.sendMessage(valueToSubmit);
1630
+ return true;
1631
+ },
1632
+ startVoiceRecognition(): boolean {
1633
+ if (isRecording || session.isStreaming()) return false;
1634
+
1635
+ const SpeechRecognitionClass = getSpeechRecognitionClass();
1636
+ if (!SpeechRecognitionClass) return false;
1637
+
1638
+ // Auto-open widget if closed and launcher is enabled
1639
+ if (!open && launcherEnabled) {
1640
+ setOpenState(true);
1641
+ }
1642
+
1643
+ startVoiceRecognition();
1644
+ return true;
1645
+ },
1646
+ stopVoiceRecognition(): boolean {
1647
+ if (!isRecording) return false;
1648
+
1649
+ stopVoiceRecognition();
1650
+ return true;
1651
+ },
1622
1652
  destroy() {
1623
1653
  destroyCallbacks.forEach((cb) => cb());
1624
1654
  wrapper.remove();