reactbridge-sdk 0.1.11 → 0.1.13

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/index.js CHANGED
@@ -126,8 +126,73 @@ const lightTheme = {
126
126
  boxShadow: '0 2px 8px rgba(0, 0, 0, 0.1)',
127
127
  };
128
128
 
129
+ // Default STT Provider using Web Speech API
130
+ class WebSpeechSTTProvider {
131
+ constructor() {
132
+ if (typeof window !== 'undefined') {
133
+ const SpeechRecognitionCtor = window.SpeechRecognition || window.webkitSpeechRecognition;
134
+ if (SpeechRecognitionCtor) {
135
+ this.recognition = new SpeechRecognitionCtor();
136
+ if (!this.recognition) {
137
+ throw new Error('Failed to initialize SpeechRecognition');
138
+ }
139
+ this.recognition.continuous = false;
140
+ this.recognition.interimResults = false;
141
+ this.recognition.lang = 'en-US';
142
+ this.recognition.onresult = (event) => {
143
+ var _a;
144
+ const transcript = event.results[0][0].transcript;
145
+ (_a = this.onResultCallback) === null || _a === void 0 ? void 0 : _a.call(this, transcript);
146
+ };
147
+ this.recognition.onerror = (event) => {
148
+ var _a;
149
+ (_a = this.onErrorCallback) === null || _a === void 0 ? void 0 : _a.call(this, new Error(`Speech recognition error: ${event.error}`));
150
+ };
151
+ }
152
+ else {
153
+ console.warn('SpeechRecognition API not supported in this browser.');
154
+ }
155
+ }
156
+ }
157
+ startRecognition() {
158
+ if (this.recognition) {
159
+ this.recognition.start();
160
+ }
161
+ else {
162
+ throw new Error('Speech recognition not supported');
163
+ }
164
+ }
165
+ stopRecognition() {
166
+ var _a;
167
+ (_a = this.recognition) === null || _a === void 0 ? void 0 : _a.stop();
168
+ }
169
+ onResult(callback) {
170
+ this.onResultCallback = callback;
171
+ }
172
+ onError(callback) {
173
+ this.onErrorCallback = callback;
174
+ }
175
+ }
176
+ // Default TTS Provider using Web Speech API
177
+ class WebSpeechTTSProvider {
178
+ speak(text) {
179
+ if (typeof window !== 'undefined' && 'speechSynthesis' in window) {
180
+ const utterance = new SpeechSynthesisUtterance(text);
181
+ window.speechSynthesis.speak(utterance);
182
+ }
183
+ else {
184
+ console.warn('Text-to-speech not supported');
185
+ }
186
+ }
187
+ stop() {
188
+ if (typeof window !== 'undefined' && 'speechSynthesis' in window) {
189
+ window.speechSynthesis.cancel();
190
+ }
191
+ }
192
+ }
193
+
129
194
  const ReactBridgeContext = React.createContext(null);
130
- function ReactBridgeProvider({ apiKey, config = {}, theme = lightTheme, children, }) {
195
+ function ReactBridgeProvider({ apiKey, config = {}, theme = lightTheme, sttProvider: initialSTTProvider, ttsProvider: initialTTSProvider, children, }) {
131
196
  const fullConfig = {
132
197
  apiKey,
133
198
  baseURL: config.baseURL,
@@ -140,10 +205,16 @@ function ReactBridgeProvider({ apiKey, config = {}, theme = lightTheme, children
140
205
  fullConfig.baseURL,
141
206
  fullConfig.timeout,
142
207
  ]);
208
+ const [sttProvider, setSTTProvider] = React.useState(initialSTTProvider || new WebSpeechSTTProvider());
209
+ const [ttsProvider, setTTSProvider] = React.useState(initialTTSProvider || new WebSpeechTTSProvider());
143
210
  const value = {
144
211
  api,
145
212
  config: fullConfig,
146
213
  theme,
214
+ sttProvider,
215
+ ttsProvider,
216
+ setSTTProvider,
217
+ setTTSProvider,
147
218
  };
148
219
  return (React.createElement(ReactBridgeContext.Provider, { value: value }, children));
149
220
  }
@@ -189,12 +260,13 @@ function getContextSummary(context) {
189
260
  return parts.join('\n');
190
261
  }
191
262
 
192
- function useReactBridge({ onIntentDetected, currentContext, onError, }) {
193
- const { api } = useReactBridgeContext();
263
+ function useReactBridge({ onIntentDetected, currentContext, onError, onSpeechStart, onSpeechEnd, onTranscript, onAgentResponse, }) {
264
+ const { api, sttProvider, ttsProvider } = useReactBridgeContext();
194
265
  const [messages, setMessages] = React.useState([]);
195
266
  const [isLoading, setIsLoading] = React.useState(false);
196
267
  const [error, setError] = React.useState(null);
197
268
  const [sessionId, setSessionId] = React.useState(null);
269
+ const [isListening, setIsListening] = React.useState(false);
198
270
  const previousContextRef = React.useRef(null);
199
271
  const lastRequestRef = React.useRef(null);
200
272
  // Inject context changes as system messages
@@ -254,6 +326,11 @@ function useReactBridge({ onIntentDetected, currentContext, onError, }) {
254
326
  toolCall: response.toolCall,
255
327
  };
256
328
  setMessages(prev => [...prev, assistantMessage]);
329
+ // Trigger TTS and callback
330
+ ttsProvider.speak(response.message);
331
+ if (onAgentResponse) {
332
+ onAgentResponse(response.message);
333
+ }
257
334
  // Step 2: If there's a tool call, execute it
258
335
  if (response.toolCall && onIntentDetected) {
259
336
  try {
@@ -269,6 +346,11 @@ function useReactBridge({ onIntentDetected, currentContext, onError, }) {
269
346
  timestamp: new Date(),
270
347
  };
271
348
  setMessages(prev => [...prev, finalMessage]);
349
+ // Trigger TTS for final response
350
+ ttsProvider.speak(resultResponse.message);
351
+ if (onAgentResponse) {
352
+ onAgentResponse(resultResponse.message);
353
+ }
272
354
  }
273
355
  }
274
356
  catch (toolError) {
@@ -311,12 +393,50 @@ function useReactBridge({ onIntentDetected, currentContext, onError, }) {
311
393
  setSessionId(null);
312
394
  setError(null);
313
395
  }, []);
396
+ const startVoiceInput = React.useCallback(() => {
397
+ setIsListening(true);
398
+ if (onSpeechStart) {
399
+ onSpeechStart();
400
+ }
401
+ sttProvider.onResult((text) => {
402
+ if (onTranscript) {
403
+ onTranscript(text);
404
+ }
405
+ sendChatQuery(text);
406
+ setIsListening(false);
407
+ if (onSpeechEnd) {
408
+ onSpeechEnd();
409
+ }
410
+ });
411
+ if (sttProvider.onError) {
412
+ sttProvider.onError((error) => {
413
+ setIsListening(false);
414
+ if (onError) {
415
+ onError(error);
416
+ }
417
+ if (onSpeechEnd) {
418
+ onSpeechEnd();
419
+ }
420
+ });
421
+ }
422
+ sttProvider.startRecognition();
423
+ }, [sttProvider, sendChatQuery, onSpeechStart, onSpeechEnd, onTranscript, onError]);
424
+ const stopVoiceInput = React.useCallback(() => {
425
+ setIsListening(false);
426
+ sttProvider.stopRecognition();
427
+ if (onSpeechEnd) {
428
+ onSpeechEnd();
429
+ }
430
+ }, [sttProvider, onSpeechEnd]);
314
431
  return {
315
432
  messages,
316
433
  isLoading,
317
434
  sendChatQuery,
318
435
  clearMessages,
319
436
  error,
437
+ isListening,
438
+ startVoiceInput,
439
+ stopVoiceInput,
320
440
  };
321
441
  }
322
442
 
@@ -325,9 +445,14 @@ const MESSAGE_ICON_SVG = (React.createElement("svg", { xmlns: "http://www.w3.org
325
445
  , height: "1em", fill: "currentColor" },
326
446
  React.createElement("path", { d: "M20 2H4c-1.1 0-1.99.9-1.99 2L2 22l4-4h14c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2z" })));
327
447
  // --- END: Dependency-Free SVG Message Icon ---
448
+ // Microphone Icon SVG
449
+ const MIC_ICON_SVG$1 = (React.createElement("svg", { xmlns: "http://www.w3.org/2000/svg", viewBox: "0 0 24 24", width: "1em", height: "1em", fill: "currentColor" },
450
+ React.createElement("path", { d: "M12 14c1.66 0 2.99-1.34 2.99-3L15 5c0-1.66-1.34-3-3-3S9 3.34 9 5v6c0 1.66 1.34 3 3 3zm5.3-3c0 3-2.54 5.1-5.3 5.1S6.7 14 6.7 11H5c0 3.41 2.72 6.23 6 6.72V21h2v-3.28c3.28-.48 6-3.3 6-6.72h-1.7z" })));
328
451
  // Default styling constants for the widget wrapper
329
452
  const defaultToggleButtonClass = "fixed bottom-6 right-6 z-40 w-14 h-14 rounded-full shadow-lg text-white bg-blue-600 hover:bg-blue-700 transition-all flex justify-center items-center cursor-pointer text-2xl";
330
453
  function ReactBridgeChatbox({ onIntentDetected, currentContext, placeholder = "Type your message...", height = "500px", width = "100%", theme: themeOverride, renderMessage, onError,
454
+ // Voice Event Props
455
+ onSpeechStart, onSpeechEnd, onTranscript, onAgentResponse,
331
456
  // NEW Widget Props
332
457
  renderMode = "basic", // Default to 'basic' (original behavior)
333
458
  defaultOpen = false, boxLocation = "bottom-right", titleText = "AI Assistant", titleIcon = null, titleTextColor, // Will use theme.colors.text or default white based on headerBgColor
@@ -336,10 +461,14 @@ toggleIcon = MESSAGE_ICON_SVG, // <<< NOW USES THE PURE SVG CONSTANT
336
461
  toggleButtonClass = defaultToggleButtonClass, toggleButtonTitle = "Open chat assistant", }) {
337
462
  const { theme: contextTheme } = useReactBridgeContext();
338
463
  const theme = Object.assign(Object.assign({}, contextTheme), themeOverride);
339
- const { messages, isLoading, sendChatQuery } = useReactBridge({
464
+ const { messages, isLoading, sendChatQuery, isListening, startVoiceInput, stopVoiceInput } = useReactBridge({
340
465
  onIntentDetected,
341
466
  currentContext,
342
467
  onError,
468
+ onSpeechStart,
469
+ onSpeechEnd,
470
+ onTranscript,
471
+ onAgentResponse,
343
472
  });
344
473
  const [inputValue, setInputValue] = React.useState("");
345
474
  // New: Manage widget open/closed state
@@ -410,7 +539,7 @@ toggleButtonClass = defaultToggleButtonClass, toggleButtonTitle = "Open chat ass
410
539
  borderTop: `1px solid ${theme.colors.border}`,
411
540
  backgroundColor: theme.colors.surface,
412
541
  } },
413
- React.createElement("div", { style: { display: "flex", gap: theme.spacing.sm } },
542
+ React.createElement("div", { style: { display: "flex", gap: theme.spacing.sm, alignItems: "center" } },
414
543
  React.createElement("input", { type: "text", value: inputValue, onChange: (e) => setInputValue(e.target.value), placeholder: placeholder, disabled: isLoading, style: {
415
544
  flex: 1,
416
545
  padding: theme.spacing.sm,
@@ -421,6 +550,20 @@ toggleButtonClass = defaultToggleButtonClass, toggleButtonTitle = "Open chat ass
421
550
  color: theme.colors.text,
422
551
  outline: "none",
423
552
  } }),
553
+ React.createElement("button", { type: "button", onClick: isListening ? stopVoiceInput : startVoiceInput, disabled: isLoading, title: isListening ? "Stop recording" : "Start voice input", style: {
554
+ padding: theme.spacing.sm,
555
+ backgroundColor: isListening ? theme.colors.error : theme.colors.secondary,
556
+ color: "#ffffff",
557
+ border: "none",
558
+ borderRadius: theme.borderRadius,
559
+ cursor: isLoading ? "not-allowed" : "pointer",
560
+ opacity: isLoading ? 0.5 : 1,
561
+ display: "flex",
562
+ alignItems: "center",
563
+ justifyContent: "center",
564
+ width: "40px",
565
+ height: "40px",
566
+ } }, MIC_ICON_SVG$1),
424
567
  React.createElement("button", { type: "submit", disabled: isLoading || !inputValue.trim(), style: {
425
568
  padding: `${theme.spacing.sm} ${theme.spacing.md}`,
426
569
  fontSize: theme.fontSizes.md,
@@ -443,9 +586,9 @@ toggleButtonClass = defaultToggleButtonClass, toggleButtonTitle = "Open chat ass
443
586
  : { bottom: "24px", left: "24px" };
444
587
  if (!isOpen) {
445
588
  // Render the toggle button when closed
446
- return (React.createElement("button", { className: toggleButtonClass, onClick: () => setIsOpen(true), title: toggleButtonTitle, style: Object.assign(Object.assign({
589
+ return (React.createElement("button", { className: toggleButtonClass, onClick: () => setIsOpen(true), title: toggleButtonTitle, style: Object.assign(Object.assign(Object.assign({
447
590
  // Apply widget-specific fixed positioning
448
- position: "fixed", zIndex: 40 }, togglePositionClass), (toggleButtonClass === defaultToggleButtonClass && {
591
+ position: "fixed", zIndex: 40 }, togglePositionClass), { backgroundColor: finalHeaderBgColor }), (toggleButtonClass === defaultToggleButtonClass && {
449
592
  width: '56px', height: '56px', borderRadius: '50%', boxShadow: '0 4px 6px rgba(0, 0, 0, 0.1)'
450
593
  })) }, toggleIcon));
451
594
  }
@@ -489,7 +632,7 @@ toggleButtonClass = defaultToggleButtonClass, toggleButtonTitle = "Open chat ass
489
632
  borderTop: `1px solid ${theme.colors.border}`,
490
633
  backgroundColor: theme.colors.surface,
491
634
  } },
492
- React.createElement("div", { style: { display: "flex", gap: theme.spacing.sm } },
635
+ React.createElement("div", { style: { display: "flex", gap: theme.spacing.sm, alignItems: "center" } },
493
636
  React.createElement("input", { type: "text", value: inputValue, onChange: (e) => setInputValue(e.target.value), placeholder: placeholder, disabled: isLoading, style: {
494
637
  flex: 1,
495
638
  padding: theme.spacing.sm,
@@ -500,6 +643,20 @@ toggleButtonClass = defaultToggleButtonClass, toggleButtonTitle = "Open chat ass
500
643
  color: theme.colors.text,
501
644
  outline: "none",
502
645
  } }),
646
+ React.createElement("button", { type: "button", onClick: isListening ? stopVoiceInput : startVoiceInput, disabled: isLoading, title: isListening ? "Stop recording" : "Start voice input", style: {
647
+ padding: theme.spacing.sm,
648
+ backgroundColor: isListening ? theme.colors.error : theme.colors.secondary,
649
+ color: "#ffffff",
650
+ border: "none",
651
+ borderRadius: theme.borderRadius,
652
+ cursor: isLoading ? "not-allowed" : "pointer",
653
+ opacity: isLoading ? 0.5 : 1,
654
+ display: "flex",
655
+ alignItems: "center",
656
+ justifyContent: "center",
657
+ width: "40px",
658
+ height: "40px",
659
+ } }, MIC_ICON_SVG$1),
503
660
  React.createElement("button", { type: "submit", disabled: isLoading || !inputValue.trim(), style: {
504
661
  padding: `${theme.spacing.sm} ${theme.spacing.md}`,
505
662
  fontSize: theme.fontSizes.md,
@@ -512,13 +669,20 @@ toggleButtonClass = defaultToggleButtonClass, toggleButtonTitle = "Open chat ass
512
669
  } }, isLoading ? "Sending..." : "Send")))));
513
670
  }
514
671
 
515
- function ReactBridgeSearch({ onIntentDetected, currentContext, placeholder = 'Search...', width = '100%', maxResults = 5, theme: themeOverride, onError, }) {
672
+ // Microphone Icon SVG
673
+ const MIC_ICON_SVG = (React.createElement("svg", { xmlns: "http://www.w3.org/2000/svg", viewBox: "0 0 24 24", width: "1em", height: "1em", fill: "currentColor" },
674
+ React.createElement("path", { d: "M12 14c1.66 0 2.99-1.34 2.99-3L15 5c0-1.66-1.34-3-3-3S9 3.34 9 5v6c0 1.66 1.34 3 3 3zm5.3-3c0 3-2.54 5.1-5.3 5.1S6.7 14 6.7 11H5c0 3.41 2.72 6.23 6 6.72V21h2v-3.28c3.28-.48 6-3.3 6-6.72h-1.7z" })));
675
+ function ReactBridgeSearch({ onIntentDetected, currentContext, placeholder = 'Search...', width = '100%', maxResults = 5, theme: themeOverride, onError, onSpeechStart, onSpeechEnd, onTranscript, onAgentResponse, }) {
516
676
  const { theme: contextTheme } = useReactBridgeContext();
517
677
  const theme = Object.assign(Object.assign({}, contextTheme), themeOverride);
518
- const { messages, isLoading, sendChatQuery } = useReactBridge({
678
+ const { messages, isLoading, sendChatQuery, isListening, startVoiceInput, stopVoiceInput } = useReactBridge({
519
679
  onIntentDetected,
520
680
  currentContext,
521
681
  onError,
682
+ onSpeechStart,
683
+ onSpeechEnd,
684
+ onTranscript,
685
+ onAgentResponse,
522
686
  });
523
687
  const [inputValue, setInputValue] = React.useState('');
524
688
  const [isOpen, setIsOpen] = React.useState(false);
@@ -560,7 +724,7 @@ function ReactBridgeSearch({ onIntentDetected, currentContext, placeholder = 'Se
560
724
  React.createElement("input", { type: "text", value: inputValue, onChange: (e) => setInputValue(e.target.value), onFocus: () => displayMessages.length > 0 && setIsOpen(true), placeholder: placeholder, disabled: isLoading, style: {
561
725
  width: '100%',
562
726
  padding: theme.spacing.md,
563
- paddingRight: '100px',
727
+ paddingRight: '140px', // Increased to make room for both mic and search buttons
564
728
  fontSize: theme.fontSizes.md,
565
729
  border: `1px solid ${theme.colors.border}`,
566
730
  borderRadius: theme.borderRadius,
@@ -569,6 +733,24 @@ function ReactBridgeSearch({ onIntentDetected, currentContext, placeholder = 'Se
569
733
  outline: 'none',
570
734
  boxSizing: 'border-box',
571
735
  } }),
736
+ React.createElement("button", { type: "button", onClick: isListening ? stopVoiceInput : startVoiceInput, disabled: isLoading, title: isListening ? "Stop recording" : "Start voice input", style: {
737
+ position: 'absolute',
738
+ right: '70px', // Position before the search button
739
+ top: '50%',
740
+ transform: 'translateY(-50%)',
741
+ padding: theme.spacing.sm,
742
+ backgroundColor: isListening ? theme.colors.error : theme.colors.secondary,
743
+ color: "#ffffff",
744
+ border: "none",
745
+ borderRadius: theme.borderRadius,
746
+ cursor: isLoading ? "not-allowed" : "pointer",
747
+ opacity: isLoading ? 0.5 : 1,
748
+ display: "flex",
749
+ alignItems: "center",
750
+ justifyContent: "center",
751
+ width: "32px",
752
+ height: "32px",
753
+ } }, MIC_ICON_SVG),
572
754
  React.createElement("button", { type: "submit", disabled: isLoading || !inputValue.trim(), style: {
573
755
  position: 'absolute',
574
756
  right: theme.spacing.sm,
@@ -654,6 +836,8 @@ exports.ReactBridgeAPI = ReactBridgeAPI;
654
836
  exports.ReactBridgeChatbox = ReactBridgeChatbox;
655
837
  exports.ReactBridgeProvider = ReactBridgeProvider;
656
838
  exports.ReactBridgeSearch = ReactBridgeSearch;
839
+ exports.WebSpeechSTTProvider = WebSpeechSTTProvider;
840
+ exports.WebSpeechTTSProvider = WebSpeechTTSProvider;
657
841
  exports.createCustomTheme = createCustomTheme;
658
842
  exports.darkTheme = darkTheme;
659
843
  exports.getTheme = getTheme;