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