reactbridge-sdk 0.1.15 → 0.1.17

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.esm.js CHANGED
@@ -60,12 +60,50 @@ class ReactBridgeAPI {
60
60
  timeoutId = setTimeout(() => controller.abort(), timeoutMs);
61
61
  }
62
62
  }
63
+ // Check if this is a multimodal request (has image or document)
64
+ const hasFile = request.image || request.document;
65
+ let headers = Object.assign({ 'X-API-Key': this.config.apiKey }, this.config.headers);
66
+ let body;
67
+ if (hasFile) {
68
+ // Use FormData for file uploads
69
+ const formData = new FormData();
70
+ // Add text fields
71
+ formData.append('userId', request.userId);
72
+ formData.append('query', request.query);
73
+ if (request.userName)
74
+ formData.append('userName', request.userName);
75
+ if (request.userPreferences)
76
+ formData.append('userPreferences', request.userPreferences);
77
+ if (request.userRecentActivity)
78
+ formData.append('userRecentActivity', request.userRecentActivity);
79
+ if (request.interfaceState)
80
+ formData.append('interfaceState', JSON.stringify(request.interfaceState));
81
+ if (request.sessionId)
82
+ formData.append('sessionId', request.sessionId);
83
+ if (request.modalityHint)
84
+ formData.append('modalityHint', request.modalityHint);
85
+ // Add files
86
+ if (request.image)
87
+ formData.append('image', request.image);
88
+ if (request.document)
89
+ formData.append('document', request.document);
90
+ body = formData;
91
+ // Don't set Content-Type for FormData - let browser set it with boundary
92
+ delete headers['Content-Type'];
93
+ }
94
+ else {
95
+ // Use JSON for text-only requests
96
+ headers['Content-Type'] = 'application/json';
97
+ body = JSON.stringify(request);
98
+ }
99
+ console.log('Sending request to ReactBridge API:', { url, headers, body, timeoutMs });
63
100
  const response = yield fetch(url, {
64
101
  method: 'POST',
65
- headers: Object.assign({ 'Content-Type': 'application/json', 'X-API-Key': this.config.apiKey }, this.config.headers),
66
- body: JSON.stringify(request),
102
+ headers,
103
+ body,
67
104
  signal,
68
105
  });
106
+ console.log('Received response from ReactBridge API:', response);
69
107
  // Clear any manual timeout timer if set
70
108
  if (timeoutId) {
71
109
  clearTimeout(timeoutId);
@@ -91,6 +129,28 @@ class ReactBridgeAPI {
91
129
  return this.sendMessage(request);
92
130
  });
93
131
  }
132
+ // Helper methods for different request types
133
+ sendTextRequest(userId, query, context) {
134
+ return __awaiter(this, void 0, void 0, function* () {
135
+ const request = Object.assign({ userId,
136
+ query }, context);
137
+ return this.sendMessage(request);
138
+ });
139
+ }
140
+ sendImageRequest(userId_1, imageFile_1) {
141
+ return __awaiter(this, arguments, void 0, function* (userId, imageFile, query = '', context) {
142
+ const request = Object.assign({ userId,
143
+ query, image: imageFile, modalityHint: 'image' }, context);
144
+ return this.sendMessage(request);
145
+ });
146
+ }
147
+ sendDocumentRequest(userId_1, documentFile_1) {
148
+ return __awaiter(this, arguments, void 0, function* (userId, documentFile, query = '', context) {
149
+ const request = Object.assign({ userId,
150
+ query, document: documentFile, modalityHint: 'document' }, context);
151
+ return this.sendMessage(request);
152
+ });
153
+ }
94
154
  }
95
155
 
96
156
  const lightTheme = {
@@ -281,16 +341,27 @@ function useReactBridge({ onIntentDetected, currentContext, onError, onSpeechSta
281
341
  previousContextRef.current = currentContext;
282
342
  }
283
343
  }, [currentContext]);
284
- const sendChatQuery = useCallback((query) => __awaiter(this, void 0, void 0, function* () {
285
- if (!query.trim())
344
+ const sendChatQuery = useCallback((queryOrOptions) => __awaiter(this, void 0, void 0, function* () {
345
+ const isMultimodal = typeof queryOrOptions === 'object';
346
+ const query = isMultimodal ? (queryOrOptions.query || '') : queryOrOptions;
347
+ const image = isMultimodal ? queryOrOptions.image : undefined;
348
+ const document = isMultimodal ? queryOrOptions.document : undefined;
349
+ if (!query.trim() && !image && !document)
286
350
  return;
287
351
  setIsLoading(true);
288
352
  setError(null);
289
- // Add user message
353
+ // Add user message with file info if present
354
+ let messageContent = query;
355
+ if (image) {
356
+ messageContent = messageContent ? `${messageContent} [Image: ${image.name}]` : `[Image: ${image.name}]`;
357
+ }
358
+ else if (document) {
359
+ messageContent = messageContent ? `${messageContent} [Document: ${document.name}]` : `[Document: ${document.name}]`;
360
+ }
290
361
  const userMessage = {
291
362
  id: `user-${Date.now()}`,
292
363
  role: 'user',
293
- content: query,
364
+ content: messageContent,
294
365
  timestamp: new Date(),
295
366
  };
296
367
  setMessages(prev => [...prev, userMessage]);
@@ -305,6 +376,10 @@ function useReactBridge({ onIntentDetected, currentContext, onError, onSpeechSta
305
376
  userRecentActivity: currentContext.userRecentActivity,
306
377
  interfaceState: currentContext.interfaceState,
307
378
  sessionId: sessionId || undefined,
379
+ // Add multimodal fields
380
+ image,
381
+ document,
382
+ modalityHint: image ? 'image' : document ? 'document' : undefined,
308
383
  };
309
384
  lastRequestRef.current = request;
310
385
  const response = yield api.sendMessage(request);
@@ -446,6 +521,68 @@ const MESSAGE_ICON_SVG = (React.createElement("svg", { xmlns: "http://www.w3.org
446
521
  // Microphone Icon SVG
447
522
  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" },
448
523
  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" })));
524
+ // Plus Icon SVG
525
+ const PLUS_ICON_SVG$1 = (React.createElement("svg", { xmlns: "http://www.w3.org/2000/svg", viewBox: "0 0 24 24", width: "1em", height: "1em", fill: "currentColor" },
526
+ React.createElement("path", { d: "M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z" })));
527
+ // Photo Icon SVG
528
+ const PHOTO_ICON_SVG = (React.createElement("svg", { xmlns: "http://www.w3.org/2000/svg", viewBox: "0 0 24 24", width: "1em", height: "1em", fill: "currentColor" },
529
+ React.createElement("path", { d: "M21 19V5c0-1.1-.9-2-2-2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2zM8.5 13.5l2.5 3.01L14.5 12l4.5 6H5l3.5-4.5z" })));
530
+ // File Icon SVG
531
+ const FILE_ICON_SVG = (React.createElement("svg", { xmlns: "http://www.w3.org/2000/svg", viewBox: "0 0 24 24", width: "1em", height: "1em", fill: "currentColor" },
532
+ React.createElement("path", { d: "M14 2H6c-1.1 0-1.99.9-1.99 2L4 20c0 1.1.89 2 1.99 2H18c1.1 0 2-.9 2-2V8l-6-6zm2 16H8v-2h8v2zm0-4H8v-2h8v2zm-3-5V3.5L18.5 9H13z" })));
533
+ // Typing Indicator Component
534
+ const TypingIndicator = ({ theme }) => (React.createElement("div", { style: {
535
+ display: "flex",
536
+ justifyContent: "flex-start",
537
+ marginBottom: theme.spacing.md,
538
+ paddingLeft: theme.spacing.md,
539
+ } },
540
+ React.createElement("div", { style: {
541
+ display: "flex",
542
+ alignItems: "center",
543
+ gap: "4px",
544
+ padding: theme.spacing.sm,
545
+ backgroundColor: theme.colors.surface,
546
+ borderRadius: theme.borderRadius,
547
+ boxShadow: theme.boxShadow,
548
+ maxWidth: "70%",
549
+ } },
550
+ React.createElement("div", { style: {
551
+ width: "8px",
552
+ height: "8px",
553
+ borderRadius: "50%",
554
+ backgroundColor: theme.colors.primary,
555
+ animation: "typing-dot 1.4s infinite ease-in-out",
556
+ animationDelay: "0s",
557
+ } }),
558
+ React.createElement("div", { style: {
559
+ width: "8px",
560
+ height: "8px",
561
+ borderRadius: "50%",
562
+ backgroundColor: theme.colors.primary,
563
+ animation: "typing-dot 1.4s infinite ease-in-out",
564
+ animationDelay: "0.2s",
565
+ } }),
566
+ React.createElement("div", { style: {
567
+ width: "8px",
568
+ height: "8px",
569
+ borderRadius: "50%",
570
+ backgroundColor: theme.colors.primary,
571
+ animation: "typing-dot 1.4s infinite ease-in-out",
572
+ animationDelay: "0.4s",
573
+ } }),
574
+ React.createElement("style", null, `
575
+ @keyframes typing-dot {
576
+ 0%, 60%, 100% {
577
+ transform: translateY(0);
578
+ opacity: 0.4;
579
+ }
580
+ 30% {
581
+ transform: translateY(-10px);
582
+ opacity: 1;
583
+ }
584
+ }
585
+ `))));
449
586
  // Default styling constants for the widget wrapper
450
587
  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";
451
588
  function ReactBridgeChatbox({ onIntentDetected, currentContext, placeholder = "Type your message...", height = "500px", width = "100%", theme: themeOverride, renderMessage, onError,
@@ -459,7 +596,7 @@ toggleIcon = MESSAGE_ICON_SVG, // <<< NOW USES THE PURE SVG CONSTANT
459
596
  toggleButtonClass = defaultToggleButtonClass, toggleButtonTitle = "Open chat assistant", }) {
460
597
  const { theme: contextTheme } = useReactBridgeContext();
461
598
  const theme = Object.assign(Object.assign({}, contextTheme), themeOverride);
462
- const { messages, isLoading, sendChatQuery, isListening, startVoiceInput, stopVoiceInput } = useReactBridge({
599
+ const { messages, isLoading, sendChatQuery, isListening, startVoiceInput, stopVoiceInput, } = useReactBridge({
463
600
  onIntentDetected,
464
601
  currentContext,
465
602
  onError,
@@ -471,7 +608,12 @@ toggleButtonClass = defaultToggleButtonClass, toggleButtonTitle = "Open chat ass
471
608
  const [inputValue, setInputValue] = useState("");
472
609
  // New: Manage widget open/closed state
473
610
  const [isOpen, setIsOpen] = useState(defaultOpen);
611
+ // Upload functionality state
612
+ const [isUploadMenuOpen, setIsUploadMenuOpen] = useState(false);
613
+ const [selectedFile, setSelectedFile] = useState(null);
614
+ const [filePreview, setFilePreview] = useState(null);
474
615
  const messagesEndRef = useRef(null);
616
+ const containerRef = useRef(null);
475
617
  // Fallback styles for header
476
618
  const finalHeaderBgColor = headerBgColor || theme.colors.primary;
477
619
  const finalTitleTextColor = titleTextColor || "#ffffff"; // Default to white for contrast
@@ -480,14 +622,92 @@ toggleButtonClass = defaultToggleButtonClass, toggleButtonTitle = "Open chat ass
480
622
  var _a;
481
623
  (_a = messagesEndRef.current) === null || _a === void 0 ? void 0 : _a.scrollIntoView({ behavior: "smooth" });
482
624
  }, [messages]);
625
+ // Close upload menu and widget when clicking outside
626
+ useEffect(() => {
627
+ const handleClickOutside = (event) => {
628
+ if (containerRef.current &&
629
+ !containerRef.current.contains(event.target)) {
630
+ setIsUploadMenuOpen(false);
631
+ if (isOpen) {
632
+ setIsOpen(false);
633
+ }
634
+ }
635
+ };
636
+ document.addEventListener("mousedown", handleClickOutside);
637
+ return () => document.removeEventListener("mousedown", handleClickOutside);
638
+ }, [isUploadMenuOpen, isOpen]);
483
639
  const handleSubmit = (e) => __awaiter(this, void 0, void 0, function* () {
484
640
  e.preventDefault();
485
- if (!inputValue.trim() || isLoading)
641
+ if ((!inputValue.trim() && !selectedFile) || isLoading)
486
642
  return;
643
+ const query = inputValue.trim();
487
644
  setInputValue("");
488
- // TODO: Integrate multimodal logic here when file is attached
489
- yield sendChatQuery(inputValue);
645
+ // Handle multimodal request
646
+ if (selectedFile && filePreview) {
647
+ if (filePreview.type === "image") {
648
+ yield sendChatQuery({ image: selectedFile, query });
649
+ }
650
+ else if (filePreview.type === "document") {
651
+ yield sendChatQuery({ document: selectedFile, query });
652
+ }
653
+ }
654
+ else {
655
+ yield sendChatQuery(query);
656
+ }
657
+ // Clear file selection after sending
658
+ setSelectedFile(null);
659
+ setFilePreview(null);
490
660
  });
661
+ // File validation and handling
662
+ const validateAndSetFile = (file, type) => {
663
+ const imageTypes = ["image/png", "image/jpeg", "image/jpg", "image/webp"];
664
+ const documentTypes = [
665
+ "application/pdf",
666
+ "application/msword",
667
+ "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
668
+ "text/plain",
669
+ ];
670
+ const allowedTypes = type === "image" ? imageTypes : documentTypes;
671
+ if (!allowedTypes.includes(file.type)) {
672
+ if (onError) {
673
+ onError(new Error(`Invalid file type. Please select a valid ${type} file.`));
674
+ }
675
+ return false;
676
+ }
677
+ setSelectedFile(file);
678
+ if (type === "image") {
679
+ const url = URL.createObjectURL(file);
680
+ setFilePreview({ type: "image", url, name: file.name });
681
+ }
682
+ else {
683
+ setFilePreview({ type: "document", name: file.name });
684
+ }
685
+ setIsUploadMenuOpen(false);
686
+ return true;
687
+ };
688
+ const handleFileSelect = (type) => {
689
+ const input = document.createElement("input");
690
+ input.type = "file";
691
+ input.accept =
692
+ type === "image"
693
+ ? "image/png,image/jpeg,image/jpg,image/webp"
694
+ : "application/pdf,application/msword,application/vnd.openxmlformats-officedocument.wordprocessingml.document,text/plain";
695
+ input.onchange = (e) => {
696
+ var _a;
697
+ const file = (_a = e.target.files) === null || _a === void 0 ? void 0 : _a[0];
698
+ if (file) {
699
+ validateAndSetFile(file, type);
700
+ }
701
+ };
702
+ input.click();
703
+ };
704
+ const removeFile = () => {
705
+ if (filePreview === null || filePreview === void 0 ? void 0 : filePreview.url) {
706
+ URL.revokeObjectURL(filePreview.url);
707
+ }
708
+ setSelectedFile(null);
709
+ setFilePreview(null);
710
+ };
491
711
  const defaultRenderMessage = (message) => {
492
712
  const isUser = message.role === "user";
493
713
  const isSystem = message.role === "system";
@@ -514,7 +734,7 @@ toggleButtonClass = defaultToggleButtonClass, toggleButtonTitle = "Open chat ass
514
734
  // --- RENDER LOGIC FOR 'BASIC' MODE (Original Behavior) ---
515
735
  if (renderMode === "basic") {
516
736
  // Renders the chat component only, using the height/width props directly
517
- return (React.createElement("div", { style: {
737
+ return (React.createElement("div", { ref: containerRef, style: {
518
738
  width,
519
739
  height,
520
740
  display: "flex",
@@ -530,14 +750,21 @@ toggleButtonClass = defaultToggleButtonClass, toggleButtonTitle = "Open chat ass
530
750
  overflowY: "auto",
531
751
  padding: theme.spacing.md,
532
752
  } },
533
- messages.map((message) => renderMessage ? renderMessage(message) : defaultRenderMessage(message)),
753
+ messages.map((message) => renderMessage
754
+ ? renderMessage(message)
755
+ : defaultRenderMessage(message)),
756
+ isLoading && React.createElement(TypingIndicator, { theme: theme }),
534
757
  React.createElement("div", { ref: messagesEndRef })),
535
758
  React.createElement("form", { onSubmit: handleSubmit, style: {
536
759
  padding: theme.spacing.md,
537
760
  borderTop: `1px solid ${theme.colors.border}`,
538
761
  backgroundColor: theme.colors.surface,
539
762
  } },
540
- React.createElement("div", { style: { display: "flex", gap: theme.spacing.sm, alignItems: "center" } },
763
+ React.createElement("div", { style: {
764
+ display: "flex",
765
+ gap: theme.spacing.sm,
766
+ alignItems: "center",
767
+ } },
541
768
  React.createElement("input", { type: "text", value: inputValue, onChange: (e) => setInputValue(e.target.value), placeholder: placeholder, disabled: isLoading, style: {
542
769
  flex: 1,
543
770
  padding: theme.spacing.sm,
@@ -548,9 +775,65 @@ toggleButtonClass = defaultToggleButtonClass, toggleButtonTitle = "Open chat ass
548
775
  color: theme.colors.text,
549
776
  outline: "none",
550
777
  } }),
778
+ React.createElement("button", { type: "button", onClick: () => setIsUploadMenuOpen(!isUploadMenuOpen), disabled: isLoading, title: "Attach file", style: {
779
+ padding: theme.spacing.sm,
780
+ backgroundColor: theme.colors.secondary,
781
+ color: "#ffffff",
782
+ border: "none",
783
+ borderRadius: theme.borderRadius,
784
+ cursor: isLoading ? "not-allowed" : "pointer",
785
+ opacity: isLoading ? 0.5 : 1,
786
+ display: "flex",
787
+ alignItems: "center",
788
+ justifyContent: "center",
789
+ width: "40px",
790
+ height: "40px",
791
+ } }, PLUS_ICON_SVG$1),
792
+ isUploadMenuOpen && (React.createElement("div", { style: {
793
+ position: "absolute",
794
+ bottom: "60px",
795
+ right: "10px",
796
+ backgroundColor: theme.colors.background,
797
+ border: `1px solid ${theme.colors.border}`,
798
+ borderRadius: theme.borderRadius,
799
+ boxShadow: theme.boxShadow,
800
+ zIndex: 1000,
801
+ minWidth: "150px",
802
+ } },
803
+ React.createElement("button", { onClick: () => handleFileSelect("image"), style: {
804
+ width: "100%",
805
+ padding: theme.spacing.md,
806
+ border: "none",
807
+ backgroundColor: "transparent",
808
+ color: theme.colors.text,
809
+ cursor: "pointer",
810
+ display: "flex",
811
+ alignItems: "center",
812
+ gap: theme.spacing.sm,
813
+ borderRadius: `${theme.borderRadius} ${theme.borderRadius} 0 0`,
814
+ } },
815
+ PHOTO_ICON_SVG,
816
+ "Upload Image"),
817
+ React.createElement("button", { onClick: () => handleFileSelect("document"), style: {
818
+ width: "100%",
819
+ padding: theme.spacing.md,
820
+ border: "none",
821
+ backgroundColor: "transparent",
822
+ color: theme.colors.text,
823
+ cursor: "pointer",
824
+ display: "flex",
825
+ alignItems: "center",
826
+ gap: theme.spacing.sm,
827
+ borderRadius: `0 0 ${theme.borderRadius} ${theme.borderRadius}`,
828
+ borderTop: `1px solid ${theme.colors.border}`,
829
+ } },
830
+ FILE_ICON_SVG,
831
+ "Upload Document"))),
551
832
  React.createElement("button", { type: "button", onClick: isListening ? stopVoiceInput : startVoiceInput, disabled: isLoading, title: isListening ? "Stop recording" : "Start voice input", style: {
552
833
  padding: theme.spacing.sm,
553
- backgroundColor: isListening ? theme.colors.error : theme.colors.secondary,
834
+ backgroundColor: isListening
835
+ ? theme.colors.error
836
+ : theme.colors.secondary,
554
837
  color: "#ffffff",
555
838
  border: "none",
556
839
  borderRadius: theme.borderRadius,
@@ -562,16 +845,55 @@ toggleButtonClass = defaultToggleButtonClass, toggleButtonTitle = "Open chat ass
562
845
  width: "40px",
563
846
  height: "40px",
564
847
  } }, MIC_ICON_SVG$1),
565
- React.createElement("button", { type: "submit", disabled: isLoading || !inputValue.trim(), style: {
848
+ React.createElement("button", { type: "submit", disabled: isLoading || (!inputValue.trim() && !selectedFile), style: {
566
849
  padding: `${theme.spacing.sm} ${theme.spacing.md}`,
567
850
  fontSize: theme.fontSizes.md,
568
851
  backgroundColor: theme.colors.primary,
569
852
  color: "#ffffff",
570
853
  border: "none",
571
854
  borderRadius: theme.borderRadius,
572
- cursor: isLoading || !inputValue.trim() ? "not-allowed" : "pointer",
573
- opacity: isLoading || !inputValue.trim() ? 0.5 : 1,
574
- } }, isLoading ? "Sending..." : "Send")))));
855
+ cursor: isLoading || (!inputValue.trim() && !selectedFile)
856
+ ? "not-allowed"
857
+ : "pointer",
858
+ opacity: isLoading || (!inputValue.trim() && !selectedFile) ? 0.5 : 1,
859
+ } }, isLoading ? "Sending..." : "Send")),
860
+ filePreview && (React.createElement("div", { style: {
861
+ marginTop: theme.spacing.sm,
862
+ padding: theme.spacing.sm,
863
+ backgroundColor: theme.colors.surface,
864
+ border: `1px solid ${theme.colors.border}`,
865
+ borderRadius: theme.borderRadius,
866
+ display: "flex",
867
+ alignItems: "center",
868
+ gap: theme.spacing.sm,
869
+ } },
870
+ filePreview.type === "image" && filePreview.url ? (React.createElement("img", { src: filePreview.url, alt: "Preview", style: {
871
+ width: "40px",
872
+ height: "40px",
873
+ objectFit: "cover",
874
+ borderRadius: theme.borderRadius,
875
+ } })) : (React.createElement("div", { style: {
876
+ width: "40px",
877
+ height: "40px",
878
+ backgroundColor: theme.colors.secondary,
879
+ borderRadius: theme.borderRadius,
880
+ display: "flex",
881
+ alignItems: "center",
882
+ justifyContent: "center",
883
+ } }, FILE_ICON_SVG)),
884
+ React.createElement("div", { style: {
885
+ flex: 1,
886
+ fontSize: theme.fontSizes.sm,
887
+ color: theme.colors.text,
888
+ } }, filePreview.name),
889
+ React.createElement("button", { type: "button", onClick: removeFile, style: {
890
+ padding: "4px",
891
+ backgroundColor: "transparent",
892
+ border: "none",
893
+ color: theme.colors.error,
894
+ cursor: "pointer",
895
+ fontSize: "18px",
896
+ }, title: "Remove file" }, "\u00D7"))))));
575
897
  }
576
898
  // --- RENDER LOGIC FOR 'STANDARD' MODE (Widget Behavior) ---
577
899
  // Determine fixed positioning class for the widget
@@ -587,11 +909,14 @@ toggleButtonClass = defaultToggleButtonClass, toggleButtonTitle = "Open chat ass
587
909
  return (React.createElement("button", { className: toggleButtonClass, onClick: () => setIsOpen(true), title: toggleButtonTitle, style: Object.assign(Object.assign(Object.assign({
588
910
  // Apply widget-specific fixed positioning
589
911
  position: "fixed", zIndex: 40 }, togglePositionClass), { backgroundColor: finalHeaderBgColor }), (toggleButtonClass === defaultToggleButtonClass && {
590
- width: '56px', height: '56px', borderRadius: '50%', boxShadow: '0 4px 6px rgba(0, 0, 0, 0.1)'
912
+ width: "56px",
913
+ height: "56px",
914
+ borderRadius: "50%",
915
+ boxShadow: "0 4px 6px rgba(0, 0, 0, 0.1)",
591
916
  })) }, toggleIcon));
592
917
  }
593
918
  // Render the full widget when open
594
- return (React.createElement("div", { style: Object.assign({ width: width, height: height, display: "flex", flexDirection: "column", boxShadow: "0 4px 12px rgba(0, 0, 0, 0.15)", borderRadius: theme.borderRadius, overflow: "hidden", fontFamily: "system-ui, -apple-system, sans-serif", position: "fixed", zIndex: 50 }, widgetPositionClass) },
919
+ return (React.createElement("div", { ref: containerRef, style: Object.assign({ width: width, height: height, display: "flex", flexDirection: "column", boxShadow: "0 4px 12px rgba(0, 0, 0, 0.15)", borderRadius: theme.borderRadius, overflow: "hidden", fontFamily: "system-ui, -apple-system, sans-serif", position: "fixed", zIndex: 50 }, widgetPositionClass) },
595
920
  React.createElement("div", { style: {
596
921
  padding: "10px 15px",
597
922
  backgroundColor: finalHeaderBgColor,
@@ -606,7 +931,11 @@ toggleButtonClass = defaultToggleButtonClass, toggleButtonTitle = "Open chat ass
606
931
  fontWeight: "bold",
607
932
  fontSize: theme.fontSizes.md,
608
933
  } },
609
- titleIcon && (React.createElement("span", { style: { marginRight: theme.spacing.sm, display: "flex", alignItems: "center" } }, titleIcon)),
934
+ titleIcon && (React.createElement("span", { style: {
935
+ marginRight: theme.spacing.sm,
936
+ display: "flex",
937
+ alignItems: "center",
938
+ } }, titleIcon)),
610
939
  titleText),
611
940
  React.createElement("button", { onClick: () => setIsOpen(false), style: {
612
941
  background: "none",
@@ -624,13 +953,18 @@ toggleButtonClass = defaultToggleButtonClass, toggleButtonTitle = "Open chat ass
624
953
  backgroundColor: theme.colors.background,
625
954
  } },
626
955
  messages.map((message) => renderMessage ? renderMessage(message) : defaultRenderMessage(message)),
956
+ isLoading && React.createElement(TypingIndicator, { theme: theme }),
627
957
  React.createElement("div", { ref: messagesEndRef })),
628
958
  React.createElement("form", { onSubmit: handleSubmit, style: {
629
959
  padding: theme.spacing.md,
630
960
  borderTop: `1px solid ${theme.colors.border}`,
631
961
  backgroundColor: theme.colors.surface,
632
962
  } },
633
- React.createElement("div", { style: { display: "flex", gap: theme.spacing.sm, alignItems: "center" } },
963
+ React.createElement("div", { style: {
964
+ display: "flex",
965
+ gap: theme.spacing.sm,
966
+ alignItems: "center",
967
+ } },
634
968
  React.createElement("input", { type: "text", value: inputValue, onChange: (e) => setInputValue(e.target.value), placeholder: placeholder, disabled: isLoading, style: {
635
969
  flex: 1,
636
970
  padding: theme.spacing.sm,
@@ -641,9 +975,67 @@ toggleButtonClass = defaultToggleButtonClass, toggleButtonTitle = "Open chat ass
641
975
  color: theme.colors.text,
642
976
  outline: "none",
643
977
  } }),
978
+ React.createElement("button", { type: "button", onClick: () => setIsUploadMenuOpen(!isUploadMenuOpen), disabled: isLoading, title: "Attach file", style: {
979
+ padding: theme.spacing.sm,
980
+ color: theme.colors.border,
981
+ border: "none",
982
+ borderRadius: theme.borderRadius,
983
+ cursor: isLoading ? "not-allowed" : "pointer",
984
+ opacity: isLoading ? 0.5 : 1,
985
+ display: "flex",
986
+ alignItems: "center",
987
+ justifyContent: "center",
988
+ width: "40px",
989
+ height: "40px",
990
+ } }, PLUS_ICON_SVG$1),
991
+ isUploadMenuOpen && (React.createElement("div", { style: {
992
+ position: "absolute",
993
+ bottom: "60px",
994
+ right: "10px",
995
+ backgroundColor: theme.colors.background,
996
+ border: `1px solid ${theme.colors.border}`,
997
+ borderRadius: theme.borderRadius,
998
+ boxShadow: theme.boxShadow,
999
+ zIndex: 1000,
1000
+ minWidth: "150px",
1001
+ } },
1002
+ React.createElement("button", { onClick: (e) => {
1003
+ e.preventDefault();
1004
+ handleFileSelect("image");
1005
+ }, style: {
1006
+ width: "100%",
1007
+ padding: theme.spacing.md,
1008
+ border: "none",
1009
+ backgroundColor: "transparent",
1010
+ color: theme.colors.text,
1011
+ cursor: "pointer",
1012
+ display: "flex",
1013
+ alignItems: "center",
1014
+ gap: theme.spacing.sm,
1015
+ borderRadius: `${theme.borderRadius} ${theme.borderRadius} 0 0`,
1016
+ } },
1017
+ PHOTO_ICON_SVG,
1018
+ "Upload Image"),
1019
+ React.createElement("button", { onClick: (e) => {
1020
+ e.preventDefault();
1021
+ handleFileSelect("document");
1022
+ }, style: {
1023
+ width: "100%",
1024
+ padding: theme.spacing.md,
1025
+ border: "none",
1026
+ backgroundColor: "transparent",
1027
+ color: theme.colors.text,
1028
+ cursor: "pointer",
1029
+ display: "flex",
1030
+ alignItems: "center",
1031
+ gap: theme.spacing.sm,
1032
+ borderRadius: `0 0 ${theme.borderRadius} ${theme.borderRadius}`,
1033
+ borderTop: `1px solid ${theme.colors.border}`,
1034
+ } },
1035
+ FILE_ICON_SVG,
1036
+ "Upload Document"))),
644
1037
  React.createElement("button", { type: "button", onClick: isListening ? stopVoiceInput : startVoiceInput, disabled: isLoading, title: isListening ? "Stop recording" : "Start voice input", style: {
645
1038
  padding: theme.spacing.sm,
646
- // backgroundColor: isListening ? theme.colors.error : theme.colors.primary,
647
1039
  color: isListening ? theme.colors.primary : theme.colors.border,
648
1040
  border: "none",
649
1041
  borderRadius: theme.borderRadius,
@@ -655,21 +1047,63 @@ toggleButtonClass = defaultToggleButtonClass, toggleButtonTitle = "Open chat ass
655
1047
  width: "40px",
656
1048
  height: "40px",
657
1049
  } }, MIC_ICON_SVG$1),
658
- React.createElement("button", { type: "submit", disabled: isLoading || !inputValue.trim(), style: {
1050
+ React.createElement("button", { type: "submit", disabled: isLoading || (!inputValue.trim() && !selectedFile), style: {
659
1051
  padding: `${theme.spacing.sm} ${theme.spacing.md}`,
660
1052
  fontSize: theme.fontSizes.md,
661
1053
  backgroundColor: theme.colors.primary,
662
1054
  color: theme.colors.background,
663
1055
  border: "none",
664
1056
  borderRadius: theme.borderRadius,
665
- cursor: isLoading || !inputValue.trim() ? "not-allowed" : "pointer",
666
- opacity: isLoading || !inputValue.trim() ? 0.5 : 1,
667
- } }, isLoading ? "Sending..." : "Send")))));
1057
+ cursor: isLoading || (!inputValue.trim() && !selectedFile)
1058
+ ? "not-allowed"
1059
+ : "pointer",
1060
+ opacity: isLoading || (!inputValue.trim() && !selectedFile) ? 0.5 : 1,
1061
+ } }, isLoading ? "Sending..." : "Send")),
1062
+ filePreview && (React.createElement("div", { style: {
1063
+ marginTop: theme.spacing.sm,
1064
+ padding: theme.spacing.sm,
1065
+ backgroundColor: theme.colors.surface,
1066
+ border: `1px solid ${theme.colors.border}`,
1067
+ borderRadius: theme.borderRadius,
1068
+ display: "flex",
1069
+ alignItems: "center",
1070
+ gap: theme.spacing.sm,
1071
+ } },
1072
+ filePreview.type === "image" && filePreview.url ? (React.createElement("img", { src: filePreview.url, alt: "Preview", style: {
1073
+ width: "40px",
1074
+ height: "40px",
1075
+ objectFit: "cover",
1076
+ borderRadius: theme.borderRadius,
1077
+ } })) : (React.createElement("div", { style: {
1078
+ width: "40px",
1079
+ height: "40px",
1080
+ backgroundColor: theme.colors.secondary,
1081
+ borderRadius: theme.borderRadius,
1082
+ display: "flex",
1083
+ alignItems: "center",
1084
+ justifyContent: "center",
1085
+ } }, FILE_ICON_SVG)),
1086
+ React.createElement("div", { style: {
1087
+ flex: 1,
1088
+ fontSize: theme.fontSizes.sm,
1089
+ color: theme.colors.text,
1090
+ } }, filePreview.name),
1091
+ React.createElement("button", { type: "button", onClick: removeFile, style: {
1092
+ padding: "4px",
1093
+ backgroundColor: "transparent",
1094
+ border: "none",
1095
+ color: theme.colors.error,
1096
+ cursor: "pointer",
1097
+ fontSize: "18px",
1098
+ }, title: "Remove file" }, "\u00D7"))))));
668
1099
  }
669
1100
 
670
1101
  // Microphone Icon SVG
671
1102
  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" },
672
1103
  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" })));
1104
+ // Plus Icon SVG
1105
+ const PLUS_ICON_SVG = (React.createElement("svg", { xmlns: "http://www.w3.org/2000/svg", viewBox: "0 0 24 24", width: "1em", height: "1em", fill: "currentColor" },
1106
+ React.createElement("path", { d: "M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z" })));
673
1107
  function ReactBridgeSearch({ onIntentDetected, currentContext, placeholder = 'Search...', width = '100%', maxResults = 5, theme: themeOverride, onError, onSpeechStart, onSpeechEnd, onTranscript, onAgentResponse, }) {
674
1108
  const { theme: contextTheme } = useReactBridgeContext();
675
1109
  const theme = Object.assign(Object.assign({}, contextTheme), themeOverride);
@@ -684,12 +1118,17 @@ function ReactBridgeSearch({ onIntentDetected, currentContext, placeholder = 'Se
684
1118
  });
685
1119
  const [inputValue, setInputValue] = useState('');
686
1120
  const [isOpen, setIsOpen] = useState(false);
1121
+ const [isUploadMenuOpen, setIsUploadMenuOpen] = useState(false);
1122
+ const [selectedFile, setSelectedFile] = useState(null);
1123
+ const [filePreview, setFilePreview] = useState(null);
687
1124
  const containerRef = useRef(null);
688
- // Close dropdown when clicking outside
1125
+ const fileInputRef = useRef(null);
1126
+ // Close dropdown and upload menu when clicking outside
689
1127
  useEffect(() => {
690
1128
  const handleClickOutside = (event) => {
691
1129
  if (containerRef.current && !containerRef.current.contains(event.target)) {
692
1130
  setIsOpen(false);
1131
+ setIsUploadMenuOpen(false);
693
1132
  }
694
1133
  };
695
1134
  document.addEventListener('mousedown', handleClickOutside);
@@ -701,12 +1140,78 @@ function ReactBridgeSearch({ onIntentDetected, currentContext, placeholder = 'Se
701
1140
  setIsOpen(true);
702
1141
  }
703
1142
  }, [messages]);
1143
+ const handleFileSelect = (type) => {
1144
+ if (fileInputRef.current) {
1145
+ fileInputRef.current.accept = type === 'image'
1146
+ ? 'image/png,image/jpeg,image/jpg,image/webp'
1147
+ : 'application/pdf,application/msword,application/vnd.openxmlformats-officedocument.wordprocessingml.document,text/plain';
1148
+ fileInputRef.current.click();
1149
+ }
1150
+ setIsUploadMenuOpen(false);
1151
+ };
1152
+ const handleFileChange = (event) => {
1153
+ var _a;
1154
+ const file = (_a = event.target.files) === null || _a === void 0 ? void 0 : _a[0];
1155
+ if (!file)
1156
+ return;
1157
+ // Validate file type
1158
+ const imageTypes = ['image/png', 'image/jpeg', 'image/jpg', 'image/webp'];
1159
+ const documentTypes = ['application/pdf', 'application/msword', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', 'text/plain'];
1160
+ const allowedTypes = file.type.startsWith('image/') ? imageTypes : documentTypes;
1161
+ if (!allowedTypes.includes(file.type)) {
1162
+ if (onError) {
1163
+ onError(new Error(`Invalid file type. Please select a valid ${file.type.startsWith('image/') ? 'image' : 'document'} file.`));
1164
+ }
1165
+ return;
1166
+ }
1167
+ // Validate file size (10MB limit)
1168
+ const maxSize = 10 * 1024 * 1024; // 10MB
1169
+ if (file.size > maxSize) {
1170
+ if (onError) {
1171
+ onError(new Error('File size too large. Please select a file smaller than 10MB.'));
1172
+ }
1173
+ return;
1174
+ }
1175
+ setSelectedFile(file);
1176
+ // Create preview for images
1177
+ if (file.type.startsWith('image/')) {
1178
+ const reader = new FileReader();
1179
+ reader.onload = (e) => {
1180
+ var _a;
1181
+ setFilePreview((_a = e.target) === null || _a === void 0 ? void 0 : _a.result);
1182
+ };
1183
+ reader.readAsDataURL(file);
1184
+ }
1185
+ else {
1186
+ // For documents, show file name
1187
+ setFilePreview(file.name);
1188
+ }
1189
+ };
1190
+ const removeFile = () => {
1191
+ setSelectedFile(null);
1192
+ setFilePreview(null);
1193
+ if (fileInputRef.current) {
1194
+ fileInputRef.current.value = '';
1195
+ }
1196
+ };
704
1197
  const handleSubmit = (e) => __awaiter(this, void 0, void 0, function* () {
705
1198
  e.preventDefault();
706
- if (!inputValue.trim() || isLoading)
1199
+ if ((!inputValue.trim() && !selectedFile) || isLoading)
707
1200
  return;
708
- yield sendChatQuery(inputValue);
1201
+ const query = inputValue.trim();
709
1202
  setInputValue('');
1203
+ // Handle multimodal request
1204
+ if (selectedFile && filePreview) {
1205
+ yield sendChatQuery({
1206
+ query,
1207
+ image: selectedFile.type.startsWith('image/') ? selectedFile : undefined,
1208
+ document: selectedFile.type.startsWith('image/') ? undefined : selectedFile,
1209
+ });
1210
+ removeFile();
1211
+ }
1212
+ else {
1213
+ yield sendChatQuery(query);
1214
+ }
710
1215
  });
711
1216
  // Get only assistant messages (not system messages)
712
1217
  const displayMessages = messages
@@ -719,10 +1224,11 @@ function ReactBridgeSearch({ onIntentDetected, currentContext, placeholder = 'Se
719
1224
  } },
720
1225
  React.createElement("form", { onSubmit: handleSubmit },
721
1226
  React.createElement("div", { style: { position: 'relative' } },
722
- React.createElement("input", { type: "text", value: inputValue, onChange: (e) => setInputValue(e.target.value), onFocus: () => displayMessages.length > 0 && setIsOpen(true), placeholder: placeholder, disabled: isLoading, style: {
1227
+ React.createElement("input", { ref: fileInputRef, type: "file", onChange: handleFileChange, style: { display: 'none' } }),
1228
+ React.createElement("input", { type: "text", value: inputValue, onChange: (e) => setInputValue(e.target.value), onFocus: () => displayMessages.length > 0 && setIsOpen(true), placeholder: selectedFile ? `Search with ${selectedFile.name}...` : placeholder, disabled: isLoading, style: {
723
1229
  width: '100%',
724
1230
  padding: theme.spacing.md,
725
- paddingRight: '140px', // Increased to make room for both mic and search buttons
1231
+ paddingRight: '180px', // Increased to make room for plus, mic, and search buttons
726
1232
  fontSize: theme.fontSizes.md,
727
1233
  border: `1px solid ${theme.colors.border}`,
728
1234
  borderRadius: theme.borderRadius,
@@ -731,6 +1237,56 @@ function ReactBridgeSearch({ onIntentDetected, currentContext, placeholder = 'Se
731
1237
  outline: 'none',
732
1238
  boxSizing: 'border-box',
733
1239
  } }),
1240
+ React.createElement("button", { type: "button", onClick: () => setIsUploadMenuOpen(!isUploadMenuOpen), disabled: isLoading, title: "Upload file", style: {
1241
+ position: 'absolute',
1242
+ right: '105px', // Position before the mic button
1243
+ top: '50%',
1244
+ transform: 'translateY(-50%)',
1245
+ padding: theme.spacing.sm,
1246
+ marginRight: theme.spacing.xs,
1247
+ color: theme.colors.border,
1248
+ border: "none",
1249
+ borderRadius: theme.borderRadius,
1250
+ cursor: isLoading ? "not-allowed" : "pointer",
1251
+ opacity: isLoading ? 0.5 : 1,
1252
+ display: "flex",
1253
+ alignItems: "center",
1254
+ justifyContent: "center",
1255
+ width: "32px",
1256
+ height: "32px",
1257
+ } }, PLUS_ICON_SVG),
1258
+ isUploadMenuOpen && (React.createElement("div", { style: {
1259
+ position: 'absolute',
1260
+ right: '105px',
1261
+ top: '100%',
1262
+ marginTop: theme.spacing.xs,
1263
+ backgroundColor: theme.colors.background,
1264
+ border: `1px solid ${theme.colors.border}`,
1265
+ borderRadius: theme.borderRadius,
1266
+ boxShadow: theme.boxShadow,
1267
+ zIndex: 1000,
1268
+ minWidth: '120px',
1269
+ } },
1270
+ React.createElement("button", { type: "button", onClick: () => handleFileSelect('image'), style: {
1271
+ width: '100%',
1272
+ padding: theme.spacing.sm,
1273
+ backgroundColor: 'transparent',
1274
+ border: 'none',
1275
+ color: theme.colors.text,
1276
+ cursor: 'pointer',
1277
+ textAlign: 'left',
1278
+ fontSize: theme.fontSizes.sm,
1279
+ } }, "\uD83D\uDCF7 Image"),
1280
+ React.createElement("button", { type: "button", onClick: () => handleFileSelect('document'), style: {
1281
+ width: '100%',
1282
+ padding: theme.spacing.sm,
1283
+ backgroundColor: 'transparent',
1284
+ border: 'none',
1285
+ color: theme.colors.text,
1286
+ cursor: 'pointer',
1287
+ textAlign: 'left',
1288
+ fontSize: theme.fontSizes.sm,
1289
+ } }, "\uD83D\uDCC4 Document"))),
734
1290
  React.createElement("button", { type: "button", onClick: isListening ? stopVoiceInput : startVoiceInput, disabled: isLoading, title: isListening ? "Stop recording" : "Start voice input", style: {
735
1291
  position: 'absolute',
736
1292
  right: '70px', // Position before the search button
@@ -738,7 +1294,6 @@ function ReactBridgeSearch({ onIntentDetected, currentContext, placeholder = 'Se
738
1294
  transform: 'translateY(-50%)',
739
1295
  padding: theme.spacing.sm,
740
1296
  marginRight: theme.spacing.xs,
741
- // backgroundColor: isListening ? theme.colors.error : theme.colors.primary,
742
1297
  color: isListening ? theme.colors.primary : theme.colors.border,
743
1298
  border: "none",
744
1299
  borderRadius: theme.borderRadius,
@@ -750,7 +1305,7 @@ function ReactBridgeSearch({ onIntentDetected, currentContext, placeholder = 'Se
750
1305
  width: "32px",
751
1306
  height: "32px",
752
1307
  } }, MIC_ICON_SVG),
753
- React.createElement("button", { type: "submit", disabled: isLoading || !inputValue.trim(), style: {
1308
+ React.createElement("button", { type: "submit", disabled: isLoading || (!inputValue.trim() && !selectedFile), style: {
754
1309
  position: 'absolute',
755
1310
  right: theme.spacing.sm,
756
1311
  top: '50%',
@@ -761,9 +1316,54 @@ function ReactBridgeSearch({ onIntentDetected, currentContext, placeholder = 'Se
761
1316
  color: theme.colors.background,
762
1317
  border: 'none',
763
1318
  borderRadius: theme.borderRadius,
764
- cursor: isLoading || !inputValue.trim() ? 'not-allowed' : 'pointer',
765
- opacity: isLoading || !inputValue.trim() ? 0.5 : 1,
766
- } }, isLoading ? 'Searching...' : 'Search'))),
1319
+ cursor: isLoading || (!inputValue.trim() && !selectedFile) ? 'not-allowed' : 'pointer',
1320
+ opacity: isLoading || (!inputValue.trim() && !selectedFile) ? 0.5 : 1,
1321
+ } }, isLoading ? 'Searching...' : 'Search')),
1322
+ selectedFile && filePreview && (React.createElement("div", { style: {
1323
+ marginTop: theme.spacing.sm,
1324
+ padding: theme.spacing.sm,
1325
+ backgroundColor: theme.colors.surface,
1326
+ border: `1px solid ${theme.colors.border}`,
1327
+ borderRadius: theme.borderRadius,
1328
+ display: 'flex',
1329
+ alignItems: 'center',
1330
+ gap: theme.spacing.sm,
1331
+ } },
1332
+ selectedFile.type.startsWith('image/') ? (React.createElement("img", { src: filePreview, alt: "Preview", style: {
1333
+ width: '40px',
1334
+ height: '40px',
1335
+ objectFit: 'cover',
1336
+ borderRadius: theme.borderRadius,
1337
+ } })) : (React.createElement("div", { style: {
1338
+ width: '40px',
1339
+ height: '40px',
1340
+ backgroundColor: theme.colors.primary,
1341
+ borderRadius: theme.borderRadius,
1342
+ display: 'flex',
1343
+ alignItems: 'center',
1344
+ justifyContent: 'center',
1345
+ color: theme.colors.background,
1346
+ fontSize: theme.fontSizes.lg,
1347
+ } }, "\uD83D\uDCC4")),
1348
+ React.createElement("div", { style: { flex: 1, fontSize: theme.fontSizes.sm, color: theme.colors.text } },
1349
+ React.createElement("div", { style: { fontWeight: 'bold' } }, selectedFile.name),
1350
+ React.createElement("div", { style: { color: theme.colors.textSecondary } },
1351
+ (selectedFile.size / 1024 / 1024).toFixed(2),
1352
+ " MB")),
1353
+ React.createElement("button", { type: "button", onClick: removeFile, style: {
1354
+ padding: theme.spacing.xs,
1355
+ backgroundColor: 'transparent',
1356
+ border: 'none',
1357
+ color: theme.colors.textSecondary,
1358
+ cursor: 'pointer',
1359
+ borderRadius: theme.borderRadius,
1360
+ width: '24px',
1361
+ height: '24px',
1362
+ display: 'flex',
1363
+ alignItems: 'center',
1364
+ justifyContent: 'center',
1365
+ fontSize: '18px',
1366
+ }, title: "Remove file" }, "\u00D7")))),
767
1367
  isOpen && displayMessages.length > 0 && (React.createElement("div", { style: {
768
1368
  position: 'absolute',
769
1369
  top: '100%',