reactbridge-sdk 0.1.15 → 0.1.16

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,10 +60,46 @@ 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
+ }
63
99
  const response = yield fetch(url, {
64
100
  method: 'POST',
65
- headers: Object.assign({ 'Content-Type': 'application/json', 'X-API-Key': this.config.apiKey }, this.config.headers),
66
- body: JSON.stringify(request),
101
+ headers,
102
+ body,
67
103
  signal,
68
104
  });
69
105
  // Clear any manual timeout timer if set
@@ -91,6 +127,28 @@ class ReactBridgeAPI {
91
127
  return this.sendMessage(request);
92
128
  });
93
129
  }
130
+ // Helper methods for different request types
131
+ sendTextRequest(userId, query, context) {
132
+ return __awaiter(this, void 0, void 0, function* () {
133
+ const request = Object.assign({ userId,
134
+ query }, context);
135
+ return this.sendMessage(request);
136
+ });
137
+ }
138
+ sendImageRequest(userId_1, imageFile_1) {
139
+ return __awaiter(this, arguments, void 0, function* (userId, imageFile, query = '', context) {
140
+ const request = Object.assign({ userId,
141
+ query, image: imageFile, modalityHint: 'image' }, context);
142
+ return this.sendMessage(request);
143
+ });
144
+ }
145
+ sendDocumentRequest(userId_1, documentFile_1) {
146
+ return __awaiter(this, arguments, void 0, function* (userId, documentFile, query = '', context) {
147
+ const request = Object.assign({ userId,
148
+ query, document: documentFile, modalityHint: 'document' }, context);
149
+ return this.sendMessage(request);
150
+ });
151
+ }
94
152
  }
95
153
 
96
154
  const lightTheme = {
@@ -281,16 +339,27 @@ function useReactBridge({ onIntentDetected, currentContext, onError, onSpeechSta
281
339
  previousContextRef.current = currentContext;
282
340
  }
283
341
  }, [currentContext]);
284
- const sendChatQuery = useCallback((query) => __awaiter(this, void 0, void 0, function* () {
285
- if (!query.trim())
342
+ const sendChatQuery = useCallback((queryOrOptions) => __awaiter(this, void 0, void 0, function* () {
343
+ const isMultimodal = typeof queryOrOptions === 'object';
344
+ const query = isMultimodal ? (queryOrOptions.query || '') : queryOrOptions;
345
+ const image = isMultimodal ? queryOrOptions.image : undefined;
346
+ const document = isMultimodal ? queryOrOptions.document : undefined;
347
+ if (!query.trim() && !image && !document)
286
348
  return;
287
349
  setIsLoading(true);
288
350
  setError(null);
289
- // Add user message
351
+ // Add user message with file info if present
352
+ let messageContent = query;
353
+ if (image) {
354
+ messageContent = messageContent ? `${messageContent} [Image: ${image.name}]` : `[Image: ${image.name}]`;
355
+ }
356
+ else if (document) {
357
+ messageContent = messageContent ? `${messageContent} [Document: ${document.name}]` : `[Document: ${document.name}]`;
358
+ }
290
359
  const userMessage = {
291
360
  id: `user-${Date.now()}`,
292
361
  role: 'user',
293
- content: query,
362
+ content: messageContent,
294
363
  timestamp: new Date(),
295
364
  };
296
365
  setMessages(prev => [...prev, userMessage]);
@@ -305,6 +374,10 @@ function useReactBridge({ onIntentDetected, currentContext, onError, onSpeechSta
305
374
  userRecentActivity: currentContext.userRecentActivity,
306
375
  interfaceState: currentContext.interfaceState,
307
376
  sessionId: sessionId || undefined,
377
+ // Add multimodal fields
378
+ image,
379
+ document,
380
+ modalityHint: image ? 'image' : document ? 'document' : undefined,
308
381
  };
309
382
  lastRequestRef.current = request;
310
383
  const response = yield api.sendMessage(request);
@@ -446,6 +519,15 @@ const MESSAGE_ICON_SVG = (React.createElement("svg", { xmlns: "http://www.w3.org
446
519
  // Microphone Icon SVG
447
520
  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
521
  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" })));
522
+ // Plus Icon SVG
523
+ 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" },
524
+ React.createElement("path", { d: "M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z" })));
525
+ // Photo Icon SVG
526
+ 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" },
527
+ 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" })));
528
+ // File Icon SVG
529
+ 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" },
530
+ 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" })));
449
531
  // Default styling constants for the widget wrapper
450
532
  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
533
  function ReactBridgeChatbox({ onIntentDetected, currentContext, placeholder = "Type your message...", height = "500px", width = "100%", theme: themeOverride, renderMessage, onError,
@@ -471,7 +553,12 @@ toggleButtonClass = defaultToggleButtonClass, toggleButtonTitle = "Open chat ass
471
553
  const [inputValue, setInputValue] = useState("");
472
554
  // New: Manage widget open/closed state
473
555
  const [isOpen, setIsOpen] = useState(defaultOpen);
556
+ // Upload functionality state
557
+ const [isUploadMenuOpen, setIsUploadMenuOpen] = useState(false);
558
+ const [selectedFile, setSelectedFile] = useState(null);
559
+ const [filePreview, setFilePreview] = useState(null);
474
560
  const messagesEndRef = useRef(null);
561
+ const containerRef = useRef(null);
475
562
  // Fallback styles for header
476
563
  const finalHeaderBgColor = headerBgColor || theme.colors.primary;
477
564
  const finalTitleTextColor = titleTextColor || "#ffffff"; // Default to white for contrast
@@ -480,14 +567,83 @@ toggleButtonClass = defaultToggleButtonClass, toggleButtonTitle = "Open chat ass
480
567
  var _a;
481
568
  (_a = messagesEndRef.current) === null || _a === void 0 ? void 0 : _a.scrollIntoView({ behavior: "smooth" });
482
569
  }, [messages]);
570
+ // Close upload menu and widget when clicking outside
571
+ useEffect(() => {
572
+ const handleClickOutside = (event) => {
573
+ if (containerRef.current && !containerRef.current.contains(event.target)) {
574
+ setIsUploadMenuOpen(false);
575
+ if (isOpen) {
576
+ setIsOpen(false);
577
+ }
578
+ }
579
+ };
580
+ document.addEventListener('mousedown', handleClickOutside);
581
+ return () => document.removeEventListener('mousedown', handleClickOutside);
582
+ }, [isUploadMenuOpen, isOpen]);
483
583
  const handleSubmit = (e) => __awaiter(this, void 0, void 0, function* () {
484
584
  e.preventDefault();
485
- if (!inputValue.trim() || isLoading)
585
+ if ((!inputValue.trim() && !selectedFile) || isLoading)
486
586
  return;
587
+ const query = inputValue.trim();
487
588
  setInputValue("");
488
- // TODO: Integrate multimodal logic here when file is attached
489
- yield sendChatQuery(inputValue);
589
+ // Handle multimodal request
590
+ if (selectedFile && filePreview) {
591
+ if (filePreview.type === 'image') {
592
+ yield sendChatQuery({ image: selectedFile, query });
593
+ }
594
+ else if (filePreview.type === 'document') {
595
+ yield sendChatQuery({ document: selectedFile, query });
596
+ }
597
+ }
598
+ else {
599
+ yield sendChatQuery(query);
600
+ }
601
+ // Clear file selection after sending
602
+ setSelectedFile(null);
603
+ setFilePreview(null);
490
604
  });
605
+ // File validation and handling
606
+ const validateAndSetFile = (file, type) => {
607
+ const imageTypes = ['image/png', 'image/jpeg', 'image/jpg', 'image/webp'];
608
+ const documentTypes = ['application/pdf', 'application/msword', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', 'text/plain'];
609
+ const allowedTypes = type === 'image' ? imageTypes : documentTypes;
610
+ if (!allowedTypes.includes(file.type)) {
611
+ if (onError) {
612
+ onError(new Error(`Invalid file type. Please select a valid ${type} file.`));
613
+ }
614
+ return false;
615
+ }
616
+ setSelectedFile(file);
617
+ if (type === 'image') {
618
+ const url = URL.createObjectURL(file);
619
+ setFilePreview({ type: 'image', url, name: file.name });
620
+ }
621
+ else {
622
+ setFilePreview({ type: 'document', name: file.name });
623
+ }
624
+ setIsUploadMenuOpen(false);
625
+ return true;
626
+ };
627
+ const handleFileSelect = (type) => {
628
+ const input = document.createElement('input');
629
+ input.type = 'file';
630
+ input.accept = type === 'image' ? 'image/png,image/jpeg,image/jpg,image/webp' : 'application/pdf,application/msword,application/vnd.openxmlformats-officedocument.wordprocessingml.document,text/plain';
631
+ input.onchange = (e) => {
632
+ var _a;
633
+ const file = (_a = e.target.files) === null || _a === void 0 ? void 0 : _a[0];
634
+ if (file) {
635
+ validateAndSetFile(file, type);
636
+ }
637
+ };
638
+ input.click();
639
+ };
640
+ const removeFile = () => {
641
+ if (filePreview === null || filePreview === void 0 ? void 0 : filePreview.url) {
642
+ URL.revokeObjectURL(filePreview.url);
643
+ }
644
+ setSelectedFile(null);
645
+ setFilePreview(null);
646
+ };
491
647
  const defaultRenderMessage = (message) => {
492
648
  const isUser = message.role === "user";
493
649
  const isSystem = message.role === "system";
@@ -514,7 +670,7 @@ toggleButtonClass = defaultToggleButtonClass, toggleButtonTitle = "Open chat ass
514
670
  // --- RENDER LOGIC FOR 'BASIC' MODE (Original Behavior) ---
515
671
  if (renderMode === "basic") {
516
672
  // Renders the chat component only, using the height/width props directly
517
- return (React.createElement("div", { style: {
673
+ return (React.createElement("div", { ref: containerRef, style: {
518
674
  width,
519
675
  height,
520
676
  display: "flex",
@@ -548,6 +704,60 @@ toggleButtonClass = defaultToggleButtonClass, toggleButtonTitle = "Open chat ass
548
704
  color: theme.colors.text,
549
705
  outline: "none",
550
706
  } }),
707
+ React.createElement("button", { type: "button", onClick: () => setIsUploadMenuOpen(!isUploadMenuOpen), disabled: isLoading, title: "Attach file", style: {
708
+ padding: theme.spacing.sm,
709
+ backgroundColor: theme.colors.secondary,
710
+ color: "#ffffff",
711
+ border: "none",
712
+ borderRadius: theme.borderRadius,
713
+ cursor: isLoading ? "not-allowed" : "pointer",
714
+ opacity: isLoading ? 0.5 : 1,
715
+ display: "flex",
716
+ alignItems: "center",
717
+ justifyContent: "center",
718
+ width: "40px",
719
+ height: "40px",
720
+ } }, PLUS_ICON_SVG$1),
721
+ isUploadMenuOpen && (React.createElement("div", { style: {
722
+ position: 'absolute',
723
+ bottom: '60px',
724
+ right: '10px',
725
+ backgroundColor: theme.colors.background,
726
+ border: `1px solid ${theme.colors.border}`,
727
+ borderRadius: theme.borderRadius,
728
+ boxShadow: theme.boxShadow,
729
+ zIndex: 1000,
730
+ minWidth: '150px',
731
+ } },
732
+ React.createElement("button", { onClick: () => handleFileSelect('image'), style: {
733
+ width: '100%',
734
+ padding: theme.spacing.md,
735
+ border: 'none',
736
+ backgroundColor: 'transparent',
737
+ color: theme.colors.text,
738
+ cursor: 'pointer',
739
+ display: 'flex',
740
+ alignItems: 'center',
741
+ gap: theme.spacing.sm,
742
+ borderRadius: `${theme.borderRadius} ${theme.borderRadius} 0 0`,
743
+ } },
744
+ PHOTO_ICON_SVG,
745
+ "Upload Image"),
746
+ React.createElement("button", { onClick: () => handleFileSelect('document'), style: {
747
+ width: '100%',
748
+ padding: theme.spacing.md,
749
+ border: 'none',
750
+ backgroundColor: 'transparent',
751
+ color: theme.colors.text,
752
+ cursor: 'pointer',
753
+ display: 'flex',
754
+ alignItems: 'center',
755
+ gap: theme.spacing.sm,
756
+ borderRadius: `0 0 ${theme.borderRadius} ${theme.borderRadius}`,
757
+ borderTop: `1px solid ${theme.colors.border}`,
758
+ } },
759
+ FILE_ICON_SVG,
760
+ "Upload Document"))),
551
761
  React.createElement("button", { type: "button", onClick: isListening ? stopVoiceInput : startVoiceInput, disabled: isLoading, title: isListening ? "Stop recording" : "Start voice input", style: {
552
762
  padding: theme.spacing.sm,
553
763
  backgroundColor: isListening ? theme.colors.error : theme.colors.secondary,
@@ -562,16 +772,41 @@ toggleButtonClass = defaultToggleButtonClass, toggleButtonTitle = "Open chat ass
562
772
  width: "40px",
563
773
  height: "40px",
564
774
  } }, MIC_ICON_SVG$1),
565
- React.createElement("button", { type: "submit", disabled: isLoading || !inputValue.trim(), style: {
775
+ React.createElement("button", { type: "submit", disabled: isLoading || (!inputValue.trim() && !selectedFile), style: {
566
776
  padding: `${theme.spacing.sm} ${theme.spacing.md}`,
567
777
  fontSize: theme.fontSizes.md,
568
778
  backgroundColor: theme.colors.primary,
569
779
  color: "#ffffff",
570
780
  border: "none",
571
781
  borderRadius: theme.borderRadius,
572
- cursor: isLoading || !inputValue.trim() ? "not-allowed" : "pointer",
573
- opacity: isLoading || !inputValue.trim() ? 0.5 : 1,
574
- } }, isLoading ? "Sending..." : "Send")))));
782
+ cursor: isLoading || (!inputValue.trim() && !selectedFile) ? "not-allowed" : "pointer",
783
+ opacity: isLoading || (!inputValue.trim() && !selectedFile) ? 0.5 : 1,
784
+ } }, isLoading ? "Sending..." : "Send")),
785
+ filePreview && (React.createElement("div", { style: {
786
+ marginTop: theme.spacing.sm,
787
+ padding: theme.spacing.sm,
788
+ backgroundColor: theme.colors.surface,
789
+ border: `1px solid ${theme.colors.border}`,
790
+ borderRadius: theme.borderRadius,
791
+ display: 'flex',
792
+ alignItems: 'center',
793
+ gap: theme.spacing.sm,
794
+ } },
795
+ filePreview.type === 'image' && filePreview.url ? (React.createElement("img", { src: filePreview.url, alt: "Preview", style: {
796
+ width: '40px',
797
+ height: '40px',
798
+ objectFit: 'cover',
799
+ borderRadius: theme.borderRadius,
800
+ } })) : (React.createElement("div", { style: { width: '40px', height: '40px', backgroundColor: theme.colors.secondary, borderRadius: theme.borderRadius, display: 'flex', alignItems: 'center', justifyContent: 'center' } }, FILE_ICON_SVG)),
801
+ React.createElement("div", { style: { flex: 1, fontSize: theme.fontSizes.sm, color: theme.colors.text } }, filePreview.name),
802
+ React.createElement("button", { type: "button", onClick: removeFile, style: {
803
+ padding: '4px',
804
+ backgroundColor: 'transparent',
805
+ border: 'none',
806
+ color: theme.colors.error,
807
+ cursor: 'pointer',
808
+ fontSize: '18px',
809
+ }, title: "Remove file" }, "\u00D7"))))));
575
810
  }
576
811
  // --- RENDER LOGIC FOR 'STANDARD' MODE (Widget Behavior) ---
577
812
  // Determine fixed positioning class for the widget
@@ -591,7 +826,7 @@ toggleButtonClass = defaultToggleButtonClass, toggleButtonTitle = "Open chat ass
591
826
  })) }, toggleIcon));
592
827
  }
593
828
  // 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) },
829
+ 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
830
  React.createElement("div", { style: {
596
831
  padding: "10px 15px",
597
832
  backgroundColor: finalHeaderBgColor,
@@ -641,9 +876,62 @@ toggleButtonClass = defaultToggleButtonClass, toggleButtonTitle = "Open chat ass
641
876
  color: theme.colors.text,
642
877
  outline: "none",
643
878
  } }),
879
+ React.createElement("button", { type: "button", onClick: () => setIsUploadMenuOpen(!isUploadMenuOpen), disabled: isLoading, title: "Attach file", style: {
880
+ padding: theme.spacing.sm,
881
+ backgroundColor: theme.colors.secondary,
882
+ color: "#ffffff",
883
+ border: "none",
884
+ borderRadius: theme.borderRadius,
885
+ cursor: isLoading ? "not-allowed" : "pointer",
886
+ opacity: isLoading ? 0.5 : 1,
887
+ display: "flex",
888
+ alignItems: "center",
889
+ justifyContent: "center",
890
+ width: "40px",
891
+ height: "40px",
892
+ } }, PLUS_ICON_SVG$1),
893
+ isUploadMenuOpen && (React.createElement("div", { style: {
894
+ position: 'absolute',
895
+ bottom: '60px',
896
+ right: '10px',
897
+ backgroundColor: theme.colors.background,
898
+ border: `1px solid ${theme.colors.border}`,
899
+ borderRadius: theme.borderRadius,
900
+ boxShadow: theme.boxShadow,
901
+ zIndex: 1000,
902
+ minWidth: '150px',
903
+ } },
904
+ React.createElement("button", { onClick: () => handleFileSelect('image'), style: {
905
+ width: '100%',
906
+ padding: theme.spacing.md,
907
+ border: 'none',
908
+ backgroundColor: 'transparent',
909
+ color: theme.colors.text,
910
+ cursor: 'pointer',
911
+ display: 'flex',
912
+ alignItems: 'center',
913
+ gap: theme.spacing.sm,
914
+ borderRadius: `${theme.borderRadius} ${theme.borderRadius} 0 0`,
915
+ } },
916
+ PHOTO_ICON_SVG,
917
+ "Upload Image"),
918
+ React.createElement("button", { onClick: () => handleFileSelect('document'), style: {
919
+ width: '100%',
920
+ padding: theme.spacing.md,
921
+ border: 'none',
922
+ backgroundColor: 'transparent',
923
+ color: theme.colors.text,
924
+ cursor: 'pointer',
925
+ display: 'flex',
926
+ alignItems: 'center',
927
+ gap: theme.spacing.sm,
928
+ borderRadius: `0 0 ${theme.borderRadius} ${theme.borderRadius}`,
929
+ borderTop: `1px solid ${theme.colors.border}`,
930
+ } },
931
+ FILE_ICON_SVG,
932
+ "Upload Document"))),
644
933
  React.createElement("button", { type: "button", onClick: isListening ? stopVoiceInput : startVoiceInput, disabled: isLoading, title: isListening ? "Stop recording" : "Start voice input", style: {
645
934
  padding: theme.spacing.sm,
646
- // backgroundColor: isListening ? theme.colors.error : theme.colors.primary,
647
935
  color: isListening ? theme.colors.primary : theme.colors.border,
648
936
  border: "none",
649
937
  borderRadius: theme.borderRadius,
@@ -655,21 +943,49 @@ toggleButtonClass = defaultToggleButtonClass, toggleButtonTitle = "Open chat ass
655
943
  width: "40px",
656
944
  height: "40px",
657
945
  } }, MIC_ICON_SVG$1),
658
- React.createElement("button", { type: "submit", disabled: isLoading || !inputValue.trim(), style: {
946
+ React.createElement("button", { type: "submit", disabled: isLoading || (!inputValue.trim() && !selectedFile), style: {
659
947
  padding: `${theme.spacing.sm} ${theme.spacing.md}`,
660
948
  fontSize: theme.fontSizes.md,
661
949
  backgroundColor: theme.colors.primary,
662
950
  color: theme.colors.background,
663
951
  border: "none",
664
952
  borderRadius: theme.borderRadius,
665
- cursor: isLoading || !inputValue.trim() ? "not-allowed" : "pointer",
666
- opacity: isLoading || !inputValue.trim() ? 0.5 : 1,
667
- } }, isLoading ? "Sending..." : "Send")))));
953
+ cursor: isLoading || (!inputValue.trim() && !selectedFile) ? "not-allowed" : "pointer",
954
+ opacity: isLoading || (!inputValue.trim() && !selectedFile) ? 0.5 : 1,
955
+ } }, isLoading ? "Sending..." : "Send")),
956
+ filePreview && (React.createElement("div", { style: {
957
+ marginTop: theme.spacing.sm,
958
+ padding: theme.spacing.sm,
959
+ backgroundColor: theme.colors.surface,
960
+ border: `1px solid ${theme.colors.border}`,
961
+ borderRadius: theme.borderRadius,
962
+ display: 'flex',
963
+ alignItems: 'center',
964
+ gap: theme.spacing.sm,
965
+ } },
966
+ filePreview.type === 'image' && filePreview.url ? (React.createElement("img", { src: filePreview.url, alt: "Preview", style: {
967
+ width: '40px',
968
+ height: '40px',
969
+ objectFit: 'cover',
970
+ borderRadius: theme.borderRadius,
971
+ } })) : (React.createElement("div", { style: { width: '40px', height: '40px', backgroundColor: theme.colors.secondary, borderRadius: theme.borderRadius, display: 'flex', alignItems: 'center', justifyContent: 'center' } }, FILE_ICON_SVG)),
972
+ React.createElement("div", { style: { flex: 1, fontSize: theme.fontSizes.sm, color: theme.colors.text } }, filePreview.name),
973
+ React.createElement("button", { type: "button", onClick: removeFile, style: {
974
+ padding: '4px',
975
+ backgroundColor: 'transparent',
976
+ border: 'none',
977
+ color: theme.colors.error,
978
+ cursor: 'pointer',
979
+ fontSize: '18px',
980
+ }, title: "Remove file" }, "\u00D7"))))));
668
981
  }
669
982
 
670
983
  // Microphone Icon SVG
671
984
  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
985
  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" })));
986
+ // Plus Icon SVG
987
+ 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" },
988
+ React.createElement("path", { d: "M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z" })));
673
989
  function ReactBridgeSearch({ onIntentDetected, currentContext, placeholder = 'Search...', width = '100%', maxResults = 5, theme: themeOverride, onError, onSpeechStart, onSpeechEnd, onTranscript, onAgentResponse, }) {
674
990
  const { theme: contextTheme } = useReactBridgeContext();
675
991
  const theme = Object.assign(Object.assign({}, contextTheme), themeOverride);
@@ -684,12 +1000,17 @@ function ReactBridgeSearch({ onIntentDetected, currentContext, placeholder = 'Se
684
1000
  });
685
1001
  const [inputValue, setInputValue] = useState('');
686
1002
  const [isOpen, setIsOpen] = useState(false);
1003
+ const [isUploadMenuOpen, setIsUploadMenuOpen] = useState(false);
1004
+ const [selectedFile, setSelectedFile] = useState(null);
1005
+ const [filePreview, setFilePreview] = useState(null);
687
1006
  const containerRef = useRef(null);
688
- // Close dropdown when clicking outside
1007
+ const fileInputRef = useRef(null);
1008
+ // Close dropdown and upload menu when clicking outside
689
1009
  useEffect(() => {
690
1010
  const handleClickOutside = (event) => {
691
1011
  if (containerRef.current && !containerRef.current.contains(event.target)) {
692
1012
  setIsOpen(false);
1013
+ setIsUploadMenuOpen(false);
693
1014
  }
694
1015
  };
695
1016
  document.addEventListener('mousedown', handleClickOutside);
@@ -701,12 +1022,78 @@ function ReactBridgeSearch({ onIntentDetected, currentContext, placeholder = 'Se
701
1022
  setIsOpen(true);
702
1023
  }
703
1024
  }, [messages]);
1025
+ const handleFileSelect = (type) => {
1026
+ if (fileInputRef.current) {
1027
+ fileInputRef.current.accept = type === 'image'
1028
+ ? 'image/png,image/jpeg,image/jpg,image/webp'
1029
+ : 'application/pdf,application/msword,application/vnd.openxmlformats-officedocument.wordprocessingml.document,text/plain';
1030
+ fileInputRef.current.click();
1031
+ }
1032
+ setIsUploadMenuOpen(false);
1033
+ };
1034
+ const handleFileChange = (event) => {
1035
+ var _a;
1036
+ const file = (_a = event.target.files) === null || _a === void 0 ? void 0 : _a[0];
1037
+ if (!file)
1038
+ return;
1039
+ // Validate file type
1040
+ const imageTypes = ['image/png', 'image/jpeg', 'image/jpg', 'image/webp'];
1041
+ const documentTypes = ['application/pdf', 'application/msword', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', 'text/plain'];
1042
+ const allowedTypes = file.type.startsWith('image/') ? imageTypes : documentTypes;
1043
+ if (!allowedTypes.includes(file.type)) {
1044
+ if (onError) {
1045
+ onError(new Error(`Invalid file type. Please select a valid ${file.type.startsWith('image/') ? 'image' : 'document'} file.`));
1046
+ }
1047
+ return;
1048
+ }
1049
+ // Validate file size (10MB limit)
1050
+ const maxSize = 10 * 1024 * 1024; // 10MB
1051
+ if (file.size > maxSize) {
1052
+ if (onError) {
1053
+ onError(new Error('File size too large. Please select a file smaller than 10MB.'));
1054
+ }
1055
+ return;
1056
+ }
1057
+ setSelectedFile(file);
1058
+ // Create preview for images
1059
+ if (file.type.startsWith('image/')) {
1060
+ const reader = new FileReader();
1061
+ reader.onload = (e) => {
1062
+ var _a;
1063
+ setFilePreview((_a = e.target) === null || _a === void 0 ? void 0 : _a.result);
1064
+ };
1065
+ reader.readAsDataURL(file);
1066
+ }
1067
+ else {
1068
+ // For documents, show file name
1069
+ setFilePreview(file.name);
1070
+ }
1071
+ };
1072
+ const removeFile = () => {
1073
+ setSelectedFile(null);
1074
+ setFilePreview(null);
1075
+ if (fileInputRef.current) {
1076
+ fileInputRef.current.value = '';
1077
+ }
1078
+ };
704
1079
  const handleSubmit = (e) => __awaiter(this, void 0, void 0, function* () {
705
1080
  e.preventDefault();
706
- if (!inputValue.trim() || isLoading)
1081
+ if ((!inputValue.trim() && !selectedFile) || isLoading)
707
1082
  return;
708
- yield sendChatQuery(inputValue);
1083
+ const query = inputValue.trim();
709
1084
  setInputValue('');
1085
+ // Handle multimodal request
1086
+ if (selectedFile && filePreview) {
1087
+ yield sendChatQuery({
1088
+ query,
1089
+ image: selectedFile.type.startsWith('image/') ? selectedFile : undefined,
1090
+ document: selectedFile.type.startsWith('image/') ? undefined : selectedFile,
1091
+ });
1092
+ removeFile();
1093
+ }
1094
+ else {
1095
+ yield sendChatQuery(query);
1096
+ }
710
1097
  });
711
1098
  // Get only assistant messages (not system messages)
712
1099
  const displayMessages = messages
@@ -719,10 +1106,11 @@ function ReactBridgeSearch({ onIntentDetected, currentContext, placeholder = 'Se
719
1106
  } },
720
1107
  React.createElement("form", { onSubmit: handleSubmit },
721
1108
  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: {
1109
+ React.createElement("input", { ref: fileInputRef, type: "file", onChange: handleFileChange, style: { display: 'none' } }),
1110
+ 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
1111
  width: '100%',
724
1112
  padding: theme.spacing.md,
725
- paddingRight: '140px', // Increased to make room for both mic and search buttons
1113
+ paddingRight: '180px', // Increased to make room for plus, mic, and search buttons
726
1114
  fontSize: theme.fontSizes.md,
727
1115
  border: `1px solid ${theme.colors.border}`,
728
1116
  borderRadius: theme.borderRadius,
@@ -731,6 +1119,56 @@ function ReactBridgeSearch({ onIntentDetected, currentContext, placeholder = 'Se
731
1119
  outline: 'none',
732
1120
  boxSizing: 'border-box',
733
1121
  } }),
1122
+ React.createElement("button", { type: "button", onClick: () => setIsUploadMenuOpen(!isUploadMenuOpen), disabled: isLoading, title: "Upload file", style: {
1123
+ position: 'absolute',
1124
+ right: '105px', // Position before the mic button
1125
+ top: '50%',
1126
+ transform: 'translateY(-50%)',
1127
+ padding: theme.spacing.sm,
1128
+ marginRight: theme.spacing.xs,
1129
+ color: theme.colors.border,
1130
+ border: "none",
1131
+ borderRadius: theme.borderRadius,
1132
+ cursor: isLoading ? "not-allowed" : "pointer",
1133
+ opacity: isLoading ? 0.5 : 1,
1134
+ display: "flex",
1135
+ alignItems: "center",
1136
+ justifyContent: "center",
1137
+ width: "32px",
1138
+ height: "32px",
1139
+ } }, PLUS_ICON_SVG),
1140
+ isUploadMenuOpen && (React.createElement("div", { style: {
1141
+ position: 'absolute',
1142
+ right: '105px',
1143
+ top: '100%',
1144
+ marginTop: theme.spacing.xs,
1145
+ backgroundColor: theme.colors.background,
1146
+ border: `1px solid ${theme.colors.border}`,
1147
+ borderRadius: theme.borderRadius,
1148
+ boxShadow: theme.boxShadow,
1149
+ zIndex: 1000,
1150
+ minWidth: '120px',
1151
+ } },
1152
+ React.createElement("button", { type: "button", onClick: () => handleFileSelect('image'), style: {
1153
+ width: '100%',
1154
+ padding: theme.spacing.sm,
1155
+ backgroundColor: 'transparent',
1156
+ border: 'none',
1157
+ color: theme.colors.text,
1158
+ cursor: 'pointer',
1159
+ textAlign: 'left',
1160
+ fontSize: theme.fontSizes.sm,
1161
+ } }, "\uD83D\uDCF7 Image"),
1162
+ React.createElement("button", { type: "button", onClick: () => handleFileSelect('document'), style: {
1163
+ width: '100%',
1164
+ padding: theme.spacing.sm,
1165
+ backgroundColor: 'transparent',
1166
+ border: 'none',
1167
+ color: theme.colors.text,
1168
+ cursor: 'pointer',
1169
+ textAlign: 'left',
1170
+ fontSize: theme.fontSizes.sm,
1171
+ } }, "\uD83D\uDCC4 Document"))),
734
1172
  React.createElement("button", { type: "button", onClick: isListening ? stopVoiceInput : startVoiceInput, disabled: isLoading, title: isListening ? "Stop recording" : "Start voice input", style: {
735
1173
  position: 'absolute',
736
1174
  right: '70px', // Position before the search button
@@ -738,7 +1176,6 @@ function ReactBridgeSearch({ onIntentDetected, currentContext, placeholder = 'Se
738
1176
  transform: 'translateY(-50%)',
739
1177
  padding: theme.spacing.sm,
740
1178
  marginRight: theme.spacing.xs,
741
- // backgroundColor: isListening ? theme.colors.error : theme.colors.primary,
742
1179
  color: isListening ? theme.colors.primary : theme.colors.border,
743
1180
  border: "none",
744
1181
  borderRadius: theme.borderRadius,
@@ -750,7 +1187,7 @@ function ReactBridgeSearch({ onIntentDetected, currentContext, placeholder = 'Se
750
1187
  width: "32px",
751
1188
  height: "32px",
752
1189
  } }, MIC_ICON_SVG),
753
- React.createElement("button", { type: "submit", disabled: isLoading || !inputValue.trim(), style: {
1190
+ React.createElement("button", { type: "submit", disabled: isLoading || (!inputValue.trim() && !selectedFile), style: {
754
1191
  position: 'absolute',
755
1192
  right: theme.spacing.sm,
756
1193
  top: '50%',
@@ -761,9 +1198,54 @@ function ReactBridgeSearch({ onIntentDetected, currentContext, placeholder = 'Se
761
1198
  color: theme.colors.background,
762
1199
  border: 'none',
763
1200
  borderRadius: theme.borderRadius,
764
- cursor: isLoading || !inputValue.trim() ? 'not-allowed' : 'pointer',
765
- opacity: isLoading || !inputValue.trim() ? 0.5 : 1,
766
- } }, isLoading ? 'Searching...' : 'Search'))),
1201
+ cursor: isLoading || (!inputValue.trim() && !selectedFile) ? 'not-allowed' : 'pointer',
1202
+ opacity: isLoading || (!inputValue.trim() && !selectedFile) ? 0.5 : 1,
1203
+ } }, isLoading ? 'Searching...' : 'Search')),
1204
+ selectedFile && filePreview && (React.createElement("div", { style: {
1205
+ marginTop: theme.spacing.sm,
1206
+ padding: theme.spacing.sm,
1207
+ backgroundColor: theme.colors.surface,
1208
+ border: `1px solid ${theme.colors.border}`,
1209
+ borderRadius: theme.borderRadius,
1210
+ display: 'flex',
1211
+ alignItems: 'center',
1212
+ gap: theme.spacing.sm,
1213
+ } },
1214
+ selectedFile.type.startsWith('image/') ? (React.createElement("img", { src: filePreview, alt: "Preview", style: {
1215
+ width: '40px',
1216
+ height: '40px',
1217
+ objectFit: 'cover',
1218
+ borderRadius: theme.borderRadius,
1219
+ } })) : (React.createElement("div", { style: {
1220
+ width: '40px',
1221
+ height: '40px',
1222
+ backgroundColor: theme.colors.primary,
1223
+ borderRadius: theme.borderRadius,
1224
+ display: 'flex',
1225
+ alignItems: 'center',
1226
+ justifyContent: 'center',
1227
+ color: theme.colors.background,
1228
+ fontSize: theme.fontSizes.lg,
1229
+ } }, "\uD83D\uDCC4")),
1230
+ React.createElement("div", { style: { flex: 1, fontSize: theme.fontSizes.sm, color: theme.colors.text } },
1231
+ React.createElement("div", { style: { fontWeight: 'bold' } }, selectedFile.name),
1232
+ React.createElement("div", { style: { color: theme.colors.textSecondary } },
1233
+ (selectedFile.size / 1024 / 1024).toFixed(2),
1234
+ " MB")),
1235
+ React.createElement("button", { type: "button", onClick: removeFile, style: {
1236
+ padding: theme.spacing.xs,
1237
+ backgroundColor: 'transparent',
1238
+ border: 'none',
1239
+ color: theme.colors.textSecondary,
1240
+ cursor: 'pointer',
1241
+ borderRadius: theme.borderRadius,
1242
+ width: '24px',
1243
+ height: '24px',
1244
+ display: 'flex',
1245
+ alignItems: 'center',
1246
+ justifyContent: 'center',
1247
+ fontSize: '18px',
1248
+ }, title: "Remove file" }, "\u00D7")))),
767
1249
  isOpen && displayMessages.length > 0 && (React.createElement("div", { style: {
768
1250
  position: 'absolute',
769
1251
  top: '100%',