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