@brainfish-ai/components 0.36.0 → 0.37.0

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.
@@ -0,0 +1,20 @@
1
+ import { default as React } from 'react';
2
+ import { SearchDictation } from './hooks/useSearchDictation';
3
+ export declare const isDictating: (dictation?: SearchDictation) => boolean;
4
+ export declare const splitPartial: (value: string, partial: string) => {
5
+ committed: string;
6
+ partial: string;
7
+ };
8
+ export declare const DictationMic: ({ onStart }: {
9
+ onStart: () => void;
10
+ }) => React.JSX.Element;
11
+ export declare const DictationToolbar: ({ dictation }: {
12
+ dictation: SearchDictation;
13
+ }) => React.JSX.Element;
14
+ export declare const DictationCaption: ({ dictation, hideError }: {
15
+ dictation: SearchDictation;
16
+ hideError?: boolean;
17
+ }) => React.JSX.Element | null;
18
+ export declare const DictationLiveRegion: ({ dictation }: {
19
+ dictation: SearchDictation;
20
+ }) => React.JSX.Element;
@@ -1,5 +1,6 @@
1
1
  import { default as React } from 'react';
2
2
  import { TextConfig, AttachedFile } from './types';
3
+ import { SearchDictation } from './hooks/useSearchDictation';
3
4
  interface FollowUpSearchBarProps {
4
5
  initialQuery?: string;
5
6
  onQueryChange: (query: string) => void;
@@ -12,6 +13,7 @@ interface FollowUpSearchBarProps {
12
13
  onAddFiles?: (files: File[]) => void;
13
14
  onRemoveFile?: (id: string) => void;
14
15
  disabled?: boolean;
16
+ dictation?: SearchDictation;
15
17
  }
16
18
  export declare const FollowUpSearchBar: React.ForwardRefExoticComponent<FollowUpSearchBarProps & React.RefAttributes<HTMLTextAreaElement>>;
17
19
  export {};
@@ -1,5 +1,6 @@
1
1
  import { default as React } from 'react';
2
2
  import { Collection, Suggestion, AttachedFile } from './types';
3
+ import { SearchDictation } from './hooks/useSearchDictation';
3
4
  export declare const PrimarySearch: React.ForwardRefExoticComponent<{
4
5
  query: string;
5
6
  onQueryChange: (query: string) => void;
@@ -34,5 +35,6 @@ export declare const PrimarySearch: React.ForwardRefExoticComponent<{
34
35
  includePageLoading?: boolean;
35
36
  includePageDisabled?: boolean;
36
37
  onIncludePageToggle?: (enabled: boolean) => Promise<void>;
38
+ dictation?: SearchDictation;
37
39
  } & React.RefAttributes<HTMLTextAreaElement>>;
38
40
  export default PrimarySearch;
@@ -8,3 +8,4 @@ export { useAccessibleColor } from './useAccessibleColor';
8
8
  export { useIncludePage } from './useIncludePage';
9
9
  export { useFileAttachment } from './useFileAttachment';
10
10
  export { useToast } from './useToast';
11
+ export { useSearchDictation } from './useSearchDictation';
@@ -0,0 +1,6 @@
1
+ export declare const composeQuery: (prefix: string, session: string) => string;
2
+ /**
3
+ * False when the transcript carries no question: empty, punctuation-only, filler, or a
4
+ * silence annotation such as "[BLANK_AUDIO]". English-only; other languages pass unless empty.
5
+ */
6
+ export declare const isMeaningfulTranscript: (text: string) => boolean;
@@ -0,0 +1,25 @@
1
+ export type DictationStatus = 'idle' | 'connecting' | 'recording' | 'transcribing' | 'error';
2
+ export type DictationErrorTone = 'danger' | 'warning';
3
+ export type SearchDictation = {
4
+ available: boolean;
5
+ status: DictationStatus;
6
+ /** Always empty in batch mode; kept for story/API compatibility. */
7
+ partial: string;
8
+ /** Always empty in batch mode; kept for story/API compatibility. */
9
+ committed: string;
10
+ error: string | null;
11
+ errorTone: DictationErrorTone | null;
12
+ elapsedSeconds: number;
13
+ levels: number[];
14
+ start: () => void;
15
+ done: () => void;
16
+ cancel: () => void;
17
+ };
18
+ export declare const useSearchDictation: ({ endpoint, headers, getText, setText, onSubmit, }: {
19
+ endpoint?: string;
20
+ headers?: Record<string, string>;
21
+ getText: () => string;
22
+ setText: (value: string) => void;
23
+ /** When set, a meaningful transcript is handed here (with typed prefix) instead of written to the box. */
24
+ onSubmit?: (query: string) => void;
25
+ }) => SearchDictation;
@@ -231,6 +231,8 @@ export interface ChatSearchProps {
231
231
  resetHandoff?: () => void;
232
232
  followUpQuestionsEndpoint?: string;
233
233
  autocompleteEndpoint?: string;
234
+ /** When set, the search box shows a mic and POSTs recorded audio here for batch transcription. */
235
+ transcriptionTokenEndpoint?: string;
234
236
  contentTypeEndpoint?: string;
235
237
  disableFollowUpQuestions?: boolean;
236
238
  enableFileAttachment?: boolean;
@@ -34,6 +34,7 @@ import { MemoizedIcon } from '../ui/icon.js';
34
34
  import { Features } from '../../lib/features.js';
35
35
  import { useToast } from './hooks/useToast.js';
36
36
  import { useFileAttachment } from './hooks/useFileAttachment.js';
37
+ import { useSearchDictation } from './hooks/useSearchDictation.js';
37
38
  import { useConversationLoader } from './hooks/useConversationLoader.js';
38
39
  import { useSubscriptionManager } from './hooks/useSubscriptionManager.js';
39
40
  import { useIncludePage } from './hooks/useIncludePage.js';
@@ -61,6 +62,7 @@ const ChatSearchComponent = forwardRef(
61
62
  feedbackReasonEndpoint,
62
63
  loadConversationEndpoint,
63
64
  autocompleteEndpoint,
65
+ transcriptionTokenEndpoint,
64
66
  disableFollowUpQuestions = false,
65
67
  enableFileAttachment: enableFileAttachmentProp = false,
66
68
  disableFeedback = false,
@@ -120,6 +122,7 @@ const ChatSearchComponent = forwardRef(
120
122
  const enableFileAttachmentFlag = useBooleanFlagValue("enableFileAttachment", false);
121
123
  const enableFileAttachment = enableFileAttachmentProp || enableFileAttachmentFlag;
122
124
  const isHistoryTabEnabled = useBooleanFlagValue(Features.HistoryTab, false);
125
+ const isTranscribeEnabled = useBooleanFlagValue(Features.TranscribeEnabled, false);
123
126
  const { toasts, showToast, dismissToast } = useToast();
124
127
  const getOrCreateConversationId = useCallback(() => {
125
128
  let conversationId = currentConversationId;
@@ -147,8 +150,23 @@ const ChatSearchComponent = forwardRef(
147
150
  const followUpSearchRef = useRef(null);
148
151
  const bottomRef = useRef(null);
149
152
  const currentReaderRef = useRef(null);
153
+ const dictation = useSearchDictation({
154
+ endpoint: isTranscribeEnabled ? transcriptionTokenEndpoint : void 0,
155
+ headers,
156
+ getText: () => showResults ? followUpQuery : query,
157
+ setText: (value) => {
158
+ if (showResults) setFollowUpQuery(value);
159
+ else setQuery(value);
160
+ },
161
+ onSubmit: (dictatedQuery) => submitDictatedQueryRef.current(dictatedQuery)
162
+ });
163
+ const dictationActive = dictation.status === "connecting" || dictation.status === "recording" || dictation.status === "transcribing";
164
+ const cancelDictationRef = useRef(dictation.cancel);
165
+ cancelDictationRef.current = dictation.cancel;
166
+ const submitDictatedQueryRef = useRef(() => {
167
+ });
150
168
  const { suggestions: autocompleteSuggestions } = useAutocomplete({
151
- query,
169
+ query: dictationActive ? "" : query,
152
170
  endpoint: autocompleteEndpoint || "",
153
171
  allowedRegions,
154
172
  collectionId: currentCollectionId,
@@ -641,6 +659,7 @@ const ChatSearchComponent = forwardRef(
641
659
  }
642
660
  };
643
661
  const createNewThread = (firstQuery) => {
662
+ dictation.cancel();
644
663
  completeActiveConversations();
645
664
  setIsViewOnly(false);
646
665
  resetHandoff?.();
@@ -669,6 +688,7 @@ const ChatSearchComponent = forwardRef(
669
688
  }
670
689
  };
671
690
  const navigateHome = () => {
691
+ dictation.cancel();
672
692
  setIsViewOnly(false);
673
693
  resetHandoff?.();
674
694
  if (currentReaderRef.current) {
@@ -714,6 +734,14 @@ const ChatSearchComponent = forwardRef(
714
734
  textarea.style.height = `${textarea.scrollHeight}px`;
715
735
  };
716
736
  const handlePrimarySearch = async (searchQuery = query.trim()) => {
737
+ if (dictation.status === "recording") {
738
+ dictation.done();
739
+ return;
740
+ }
741
+ if (dictation.status === "connecting" || dictation.status === "transcribing") {
742
+ dictation.cancel();
743
+ return;
744
+ }
717
745
  await performSearch({ searchQuery, event: { name: "Primary Field Search Submitted" } });
718
746
  };
719
747
  const handleFeedback = async (response, searchQueryId, answerIndex) => {
@@ -759,14 +787,42 @@ const ChatSearchComponent = forwardRef(
759
787
  }
760
788
  };
761
789
  const handleFollowUpSearch = async (customQuery) => {
790
+ if (dictation.status === "recording") {
791
+ dictation.done();
792
+ return;
793
+ }
794
+ if (dictation.status === "connecting" || dictation.status === "transcribing") {
795
+ dictation.cancel();
796
+ return;
797
+ }
762
798
  const queryToUse = customQuery !== void 0 ? customQuery : followUpQuery;
763
799
  if (queryToUse.trim()) {
764
- resetHandoff?.();
800
+ await submitFollowUp(queryToUse);
801
+ }
802
+ };
803
+ const submitFollowUp = async (queryToUse, metaData) => {
804
+ resetHandoff?.();
805
+ setQuery(queryToUse);
806
+ setFollowUpQuery("");
807
+ await performSearch({ searchQuery: queryToUse, event: { name: "Follow-up Search Field Submitted", metaData } });
808
+ };
809
+ const submitDictatedQuery = async (queryToUse) => {
810
+ if (isSearching) {
811
+ if (showResults) setFollowUpQuery(queryToUse);
812
+ else setQuery(queryToUse);
813
+ return;
814
+ }
815
+ if (showResults) {
816
+ await submitFollowUp(queryToUse, { inputMethod: "voice" });
817
+ } else {
765
818
  setQuery(queryToUse);
766
- setFollowUpQuery("");
767
- await performSearch({ searchQuery: queryToUse, event: { name: "Follow-up Search Field Submitted" } });
819
+ await performSearch({
820
+ searchQuery: queryToUse,
821
+ event: { name: "Primary Field Search Submitted", metaData: { inputMethod: "voice" } }
822
+ });
768
823
  }
769
824
  };
825
+ submitDictatedQueryRef.current = submitDictatedQuery;
770
826
  const handleSuggestedQuestionClick = (suggestion) => {
771
827
  if (suggestion.conversationId) {
772
828
  trackEvent?.("Recent Conversation Clicked", {
@@ -795,6 +851,7 @@ const ChatSearchComponent = forwardRef(
795
851
  const urlParams = new URLSearchParams(window.location.search);
796
852
  const urlConversationId = urlParams.get("conversation");
797
853
  if (!urlConversationId) {
854
+ cancelDictationRef.current();
798
855
  setQuery("");
799
856
  setFollowUpQuery("");
800
857
  setShowResults(false);
@@ -984,7 +1041,8 @@ const ChatSearchComponent = forwardRef(
984
1041
  includePageEnabled,
985
1042
  includePageLoading,
986
1043
  includePageDisabled,
987
- onIncludePageToggle: handleIncludePageToggle
1044
+ onIncludePageToggle: handleIncludePageToggle,
1045
+ dictation: dictation.available ? dictation : void 0
988
1046
  }
989
1047
  )
990
1048
  )
@@ -1111,7 +1169,7 @@ const ChatSearchComponent = forwardRef(
1111
1169
  offset
1112
1170
  }
1113
1171
  )),
1114
- disclaimer && isWidgetMode && !(showResults && showFollowUp) && /* @__PURE__ */ React__default.createElement(
1172
+ disclaimer?.trim() !== "" && isWidgetMode && !(showResults && showFollowUp) && /* @__PURE__ */ React__default.createElement(
1115
1173
  "div",
1116
1174
  {
1117
1175
  "data-name": "disclaimer",
@@ -1151,7 +1209,8 @@ const ChatSearchComponent = forwardRef(
1151
1209
  isUploading,
1152
1210
  onAddFiles: addFiles,
1153
1211
  onRemoveFile: removeFile,
1154
- disabled: isAwaitingGroup
1212
+ disabled: isAwaitingGroup,
1213
+ dictation: dictation.available ? dictation : void 0
1155
1214
  }
1156
1215
  );
1157
1216
  })(),
@@ -0,0 +1,68 @@
1
+ import React__default from 'react';
2
+ import { X, Check, Microphone } from '@phosphor-icons/react';
3
+ import { Button } from '../ui/button.js';
4
+ import { cn } from '../../lib/utils.js';
5
+
6
+ const isDictating = (dictation) => dictation?.status === "connecting" || dictation?.status === "recording" || dictation?.status === "transcribing";
7
+ const formatElapsed = (seconds) => {
8
+ const minutes = Math.floor(seconds / 60);
9
+ const rest = seconds % 60;
10
+ return `${minutes}:${rest.toString().padStart(2, "0")}`;
11
+ };
12
+ const DictationMic = ({ onStart }) => /* @__PURE__ */ React__default.createElement(
13
+ Button,
14
+ {
15
+ type: "button",
16
+ variant: "ghost",
17
+ size: "icon",
18
+ className: "size-8",
19
+ "aria-label": "Ask with your voice",
20
+ onClick: onStart
21
+ },
22
+ /* @__PURE__ */ React__default.createElement(Microphone, { weight: "regular", className: "size-4" })
23
+ );
24
+ const DictationToolbar = ({ dictation }) => {
25
+ const connecting = dictation.status === "connecting";
26
+ const transcribing = dictation.status === "transcribing";
27
+ const recording = dictation.status === "recording";
28
+ return /* @__PURE__ */ React__default.createElement("div", { className: "flex items-center justify-between gap-2 border-t border-border bg-background px-3 py-2 rounded-b-[4px]" }, /* @__PURE__ */ React__default.createElement("div", { className: "flex min-w-0 items-center gap-2 text-sm text-foreground" }, connecting ? /* @__PURE__ */ React__default.createElement("span", null, "Connecting microphone…") : transcribing ? /* @__PURE__ */ React__default.createElement("span", null, "Transcribing…") : /* @__PURE__ */ React__default.createElement(React__default.Fragment, null, /* @__PURE__ */ React__default.createElement("span", { className: "size-2 shrink-0 rounded-full bg-red", "aria-hidden": true }), /* @__PURE__ */ React__default.createElement("span", null, "Recording"), /* @__PURE__ */ React__default.createElement("span", { className: "tabular-nums text-subtle" }, formatElapsed(dictation.elapsedSeconds)), /* @__PURE__ */ React__default.createElement("span", { className: "flex h-4 items-center gap-px", "aria-hidden": true }, dictation.levels.map((level, index) => /* @__PURE__ */ React__default.createElement(
29
+ "span",
30
+ {
31
+ key: index,
32
+ className: "h-4 w-0.5 origin-center rounded-full bg-foreground",
33
+ style: { transform: `scaleY(${level})` }
34
+ }
35
+ ))))), /* @__PURE__ */ React__default.createElement("div", { className: "flex shrink-0 items-center gap-2" }, /* @__PURE__ */ React__default.createElement(
36
+ Button,
37
+ {
38
+ type: "button",
39
+ variant: "ghost",
40
+ size: "icon",
41
+ className: "size-8",
42
+ "aria-label": "Cancel voice input",
43
+ onClick: dictation.cancel
44
+ },
45
+ /* @__PURE__ */ React__default.createElement(X, { className: "size-4" })
46
+ ), recording && /* @__PURE__ */ React__default.createElement(Button, { size: "icon", className: "bg-primary text-primary", onClick: dictation.done, "aria-label": "Done" }, /* @__PURE__ */ React__default.createElement(Check, { weight: "bold", className: "size-4" }))));
47
+ };
48
+ const DictationCaption = ({ dictation, hideError = false }) => {
49
+ const active = isDictating(dictation);
50
+ if (!active && (!dictation.error || hideError)) return null;
51
+ const text = dictation.error ? dictation.error : dictation.status === "connecting" ? "Connecting microphone…" : dictation.status === "transcribing" ? "Turning your recording into text…" : "Press Done when you are finished. Press Esc to cancel.";
52
+ return /* @__PURE__ */ React__default.createElement(
53
+ "p",
54
+ {
55
+ className: cn(
56
+ "mt-2 text-center text-sm hidden md:block",
57
+ dictation.error ? dictation.errorTone === "warning" ? "text-warning" : "text-destructive" : "text-subtle"
58
+ )
59
+ },
60
+ text
61
+ );
62
+ };
63
+ const DictationLiveRegion = ({ dictation }) => {
64
+ const message = dictation.error ?? (dictation.status === "connecting" ? "Connecting microphone" : dictation.status === "transcribing" ? "Transcribing" : dictation.status === "recording" ? "Recording" : "");
65
+ return /* @__PURE__ */ React__default.createElement("div", { className: "sr-only", "aria-live": "polite" }, message);
66
+ };
67
+
68
+ export { DictationCaption, DictationLiveRegion, DictationMic, DictationToolbar, isDictating };
@@ -1,6 +1,7 @@
1
1
  import React__default, { useState, useRef, useEffect } from 'react';
2
- import { Image, MagnifyingGlass, ArrowRight } from '@phosphor-icons/react';
2
+ import { MagnifyingGlass, Image, ArrowRight } from '@phosphor-icons/react';
3
3
  import { motion } from 'framer-motion';
4
+ import { isDictating, DictationToolbar, DictationMic, DictationCaption, DictationLiveRegion } from './DictationChrome.js';
4
5
  import styles from './FollowUpSearchBar.module.css.js';
5
6
  import { Button } from '../ui/button.js';
6
7
  import { Textarea } from '../ui/textarea.js';
@@ -19,7 +20,8 @@ const FollowUpSearchBar = React__default.forwardRef(
19
20
  isUploading = false,
20
21
  onAddFiles,
21
22
  onRemoveFile,
22
- disabled = false
23
+ disabled = false,
24
+ dictation
23
25
  }, ref) => {
24
26
  const [localQuery, setLocalQuery] = useState(initialQuery);
25
27
  const [isDragOver, setIsDragOver] = useState(false);
@@ -90,6 +92,8 @@ const FollowUpSearchBar = React__default.forwardRef(
90
92
  const handleDragLeave = () => {
91
93
  setIsDragOver(false);
92
94
  };
95
+ const dictating = isDictating(dictation);
96
+ const errorPlaceholder = !dictating && dictation?.error && !localQuery ? dictation.error : void 0;
93
97
  return /* @__PURE__ */ React__default.createElement(
94
98
  motion.footer,
95
99
  {
@@ -98,11 +102,12 @@ const FollowUpSearchBar = React__default.forwardRef(
98
102
  transition: { duration: 0.5 },
99
103
  className: styles.followUpSearchBar
100
104
  },
101
- /* @__PURE__ */ React__default.createElement("div", { className: "container flex items-center justify-center p-3" }, /* @__PURE__ */ React__default.createElement(
105
+ /* @__PURE__ */ React__default.createElement("div", { className: "container flex items-center justify-center p-3 flex-col" }, /* @__PURE__ */ React__default.createElement(
102
106
  "div",
103
107
  {
104
108
  className: cn(
105
- "relative flex w-full flex-col rounded bg-card border border-dark-400",
109
+ "relative flex w-full flex-col rounded-md bg-background",
110
+ dictating ? "bg-yellowfin-gradient !border-2" : "border border-dark-400",
106
111
  isDragOver && enableFileAttachment && "ring-2 ring-primary"
107
112
  ),
108
113
  onDrop: handleDrop,
@@ -127,7 +132,20 @@ const FollowUpSearchBar = React__default.forwardRef(
127
132
  className: "pt-2 px-2"
128
133
  }
129
134
  ),
130
- /* @__PURE__ */ React__default.createElement("div", { className: "flex items-center gap-2 px-2" }, enableFileAttachment && /* @__PURE__ */ React__default.createElement(React__default.Fragment, null, /* @__PURE__ */ React__default.createElement(
135
+ dictating && dictation ? /* @__PURE__ */ React__default.createElement(React__default.Fragment, null, /* @__PURE__ */ React__default.createElement("div", { className: "flex min-h-16 items-center bg-surface px-3 rounded-t-[4px]" }, /* @__PURE__ */ React__default.createElement(MagnifyingGlass, { className: "mr-2 size-5 shrink-0 text-muted-foreground", "aria-hidden": true }), /* @__PURE__ */ React__default.createElement("div", { className: "relative min-w-0 flex-1" }, /* @__PURE__ */ React__default.createElement(
136
+ Textarea,
137
+ {
138
+ ref: setCombinedRef,
139
+ value: localQuery,
140
+ readOnly: true,
141
+ onChange: handleChange,
142
+ onBlur: handleBlur,
143
+ onKeyDown: handleKeyPress,
144
+ placeholder: textConfig.followUpPlaceholder,
145
+ className: "min-h-11 w-full resize-none border-0 bg-transparent px-0 py-2.5 shadow-none focus:outline-none focus:ring-0 focus-visible:ring-0",
146
+ rows: 1
147
+ }
148
+ ))), /* @__PURE__ */ React__default.createElement(DictationToolbar, { dictation })) : /* @__PURE__ */ React__default.createElement("div", { className: "flex items-center gap-2 px-2" }, enableFileAttachment && /* @__PURE__ */ React__default.createElement(React__default.Fragment, null, /* @__PURE__ */ React__default.createElement(
131
149
  "input",
132
150
  {
133
151
  type: "file",
@@ -161,12 +179,15 @@ const FollowUpSearchBar = React__default.forwardRef(
161
179
  onChange: handleChange,
162
180
  onBlur: handleBlur,
163
181
  onKeyDown: handleKeyPress,
164
- placeholder: textConfig.followUpPlaceholder,
182
+ placeholder: errorPlaceholder ?? textConfig.followUpPlaceholder,
165
183
  disabled,
166
- className: "min-h-11 w-full resize-none border-0 shadow-none bg-transparent px-0 py-2.5 focus:outline-none focus:ring-0 focus-visible:ring-0 disabled:opacity-50 disabled:cursor-not-allowed",
184
+ className: cn(
185
+ "min-h-11 w-full resize-none border-0 shadow-none bg-transparent px-0 py-2.5 focus:outline-none focus:ring-0 focus-visible:ring-0 disabled:opacity-50 disabled:cursor-not-allowed",
186
+ errorPlaceholder && (dictation?.errorTone === "warning" ? "placeholder:text-warning" : "placeholder:text-destructive")
187
+ ),
167
188
  rows: 1
168
189
  }
169
- ), /* @__PURE__ */ React__default.createElement(
190
+ ), dictation && /* @__PURE__ */ React__default.createElement(DictationMic, { onStart: dictation.start }), /* @__PURE__ */ React__default.createElement(
170
191
  Button,
171
192
  {
172
193
  type: "button",
@@ -180,7 +201,7 @@ const FollowUpSearchBar = React__default.forwardRef(
180
201
  },
181
202
  isSearching ? /* @__PURE__ */ React__default.createElement("div", { className: "animate-spin rounded-full size-4 sm:size-5 border-2 border-dark/40 !border-t-foreground" }) : /* @__PURE__ */ React__default.createElement(ArrowRight, { className: "size-6" })
182
203
  ))
183
- ))
204
+ ), dictation && /* @__PURE__ */ React__default.createElement(DictationCaption, { dictation, hideError: Boolean(errorPlaceholder) }), dictation && /* @__PURE__ */ React__default.createElement(DictationLiveRegion, { dictation }))
184
205
  );
185
206
  }
186
207
  );
@@ -2,7 +2,9 @@ import React__default, { useRef, useState, useEffect } from 'react';
2
2
  import { AnimatePresence, motion } from 'framer-motion';
3
3
  import { Globe, LockSimple, MagnifyingGlass, Image, ArrowRight, CaretRight } from '@phosphor-icons/react';
4
4
  import { getCountry } from 'countries-and-timezones';
5
+ import { isDictating, DictationToolbar, DictationMic, DictationCaption, DictationLiveRegion } from './DictationChrome.js';
5
6
  import { Button } from '../ui/button.js';
7
+ import { Textarea } from '../ui/textarea.js';
6
8
  import { AttachmentThumbnails } from './AttachmentThumbnails.js';
7
9
  import { IncludePageToggle } from './IncludePageToggle.js';
8
10
  import { Combobox } from '../combobox/combobox.js';
@@ -35,7 +37,8 @@ const PrimarySearch = React__default.forwardRef(
35
37
  includePageEnabled = false,
36
38
  includePageLoading = false,
37
39
  includePageDisabled = false,
38
- onIncludePageToggle
40
+ onIncludePageToggle,
41
+ dictation
39
42
  }, ref) => {
40
43
  const fileInputRef = useRef(null);
41
44
  const [showSuggestions, setShowSuggestions] = useState(false);
@@ -124,10 +127,6 @@ const PrimarySearch = React__default.forwardRef(
124
127
  onCollectionChange(collection.value);
125
128
  }
126
129
  };
127
- const adjustTextareaHeight = (textarea) => {
128
- textarea.style.height = "auto";
129
- textarea.style.height = `${textarea.scrollHeight}px`;
130
- };
131
130
  const handleKeyPress = (e) => {
132
131
  if (e.key === "Enter" && !e.shiftKey && !isUploading) {
133
132
  e.preventDefault();
@@ -148,7 +147,9 @@ const PrimarySearch = React__default.forwardRef(
148
147
  setShowSuggestions(true);
149
148
  }
150
149
  }, [query]);
151
- const shouldShowSuggestions = showSuggestions && query.trim() && autocompleteSuggestions.length > 0;
150
+ const dictating = isDictating(dictation);
151
+ const errorPlaceholder = !dictating && dictation?.error && !query ? dictation.error : void 0;
152
+ const shouldShowSuggestions = !dictating && showSuggestions && query.trim() && autocompleteSuggestions.length > 0;
152
153
  const dropdownVariants = {
153
154
  hidden: {
154
155
  opacity: 0,
@@ -199,8 +200,9 @@ const PrimarySearch = React__default.forwardRef(
199
200
  "div",
200
201
  {
201
202
  className: cn(
202
- "relative w-full overflow-hidden bg-background border border-border transition-all",
203
- shouldShowSuggestions ? "rounded-t-md" : "rounded-md",
203
+ "relative w-full overflow-hidden bg-background transition-all",
204
+ dictating ? "rounded-md bg-yellowfin-gradient !border-2" : "border border-border",
205
+ !dictating && (shouldShowSuggestions ? "rounded-t-md" : "rounded-md"),
204
206
  isDragOver && enableFileAttachment && "ring-2 ring-primary"
205
207
  ),
206
208
  onDrop: handleDrop,
@@ -225,27 +227,25 @@ const PrimarySearch = React__default.forwardRef(
225
227
  className: "pt-3 px-3 bg-card"
226
228
  }
227
229
  ),
228
- /* @__PURE__ */ React__default.createElement("div", { className: "p-3 flex items-center bg-card", "data-name": "search-input" }, /* @__PURE__ */ React__default.createElement(MagnifyingGlass, { className: "h-5 w-5 mr-2 text-muted-foreground", "aria-hidden": true, role: "presentation" }), /* @__PURE__ */ React__default.createElement(
229
- "textarea",
230
+ /* @__PURE__ */ React__default.createElement("div", { className: cn("p-3 flex items-start bg-surface", dictating && "min-h-16"), "data-name": "search-input" }, /* @__PURE__ */ React__default.createElement(MagnifyingGlass, { className: "size-5 mr-2 shrink-0 text-muted-foreground mt-3", "aria-hidden": true, role: "presentation" }), /* @__PURE__ */ React__default.createElement("div", { className: "relative min-w-0 flex-1" }, /* @__PURE__ */ React__default.createElement(
231
+ Textarea,
230
232
  {
233
+ id: "search-input",
231
234
  ref,
232
235
  value: query,
233
- onChange: (e) => {
234
- onQueryChange(e.target.value);
235
- adjustTextareaHeight(e.target);
236
- },
236
+ readOnly: dictating,
237
+ onChange: (e) => onQueryChange(e.target.value),
237
238
  onKeyDown: handleKeyPress,
238
239
  onFocus: () => setShowSuggestions(true),
239
- placeholder: textConfig.searchPlaceholder,
240
+ placeholder: errorPlaceholder ?? textConfig.searchPlaceholder,
240
241
  className: cn(
241
- "w-full resize-none !outline-none text-base py-1 min-h-9",
242
- "overflow-hidden border-none bg-transparent shadow-none",
243
- "placeholder:text-gray-400 text-dark-900 !ring-0"
242
+ "min-h-11 w-full resize-none border-0 bg-transparent px-0 py-2.5 shadow-none focus:outline-none focus:ring-0 focus-visible:ring-0",
243
+ errorPlaceholder && (dictation?.errorTone === "warning" ? "placeholder:text-warning" : "placeholder:text-destructive")
244
244
  ),
245
245
  rows: 1
246
246
  }
247
- ), isSearching && /* @__PURE__ */ React__default.createElement("div", { className: "flex items-center justify-center ml-2" }, /* @__PURE__ */ React__default.createElement("div", { className: "animate-spin rounded-full size-4 border-2 border-t-transparent border-primary" }))),
248
- !shouldShowSuggestions && /* @__PURE__ */ React__default.createElement("div", { className: "flex items-center justify-between px-3 py-2 border-t border-border" }, /* @__PURE__ */ React__default.createElement("div", { className: "flex items-center gap-2" }, enableFileAttachment && /* @__PURE__ */ React__default.createElement(React__default.Fragment, null, /* @__PURE__ */ React__default.createElement(
247
+ )), isSearching && /* @__PURE__ */ React__default.createElement("div", { className: "flex items-center justify-center ml-2" }, /* @__PURE__ */ React__default.createElement("div", { className: "animate-spin rounded-full size-4 border-2 border-t-transparent border-primary" }))),
248
+ dictating && dictation ? /* @__PURE__ */ React__default.createElement(DictationToolbar, { dictation }) : !shouldShowSuggestions && /* @__PURE__ */ React__default.createElement("div", { className: "flex items-center justify-between px-3 py-2 border-t border-border" }, /* @__PURE__ */ React__default.createElement("div", { className: "flex items-center gap-2" }, enableFileAttachment && /* @__PURE__ */ React__default.createElement(React__default.Fragment, null, /* @__PURE__ */ React__default.createElement(
249
249
  "input",
250
250
  {
251
251
  type: "file",
@@ -312,7 +312,7 @@ const PrimarySearch = React__default.forwardRef(
312
312
  disabled: includePageDisabled,
313
313
  onToggle: onIncludePageToggle
314
314
  }
315
- )), /* @__PURE__ */ React__default.createElement("div", { className: "flex items-center", "data-name": "search-button" }, /* @__PURE__ */ React__default.createElement(
315
+ )), /* @__PURE__ */ React__default.createElement("div", { className: "flex items-center gap-2", "data-name": "search-button" }, dictation && /* @__PURE__ */ React__default.createElement(DictationMic, { onStart: dictation.start }), /* @__PURE__ */ React__default.createElement(
316
316
  Button,
317
317
  {
318
318
  size: "sm",
@@ -337,7 +337,7 @@ const PrimarySearch = React__default.forwardRef(
337
337
  }
338
338
  )
339
339
  )))
340
- ), /* @__PURE__ */ React__default.createElement(AnimatePresence, { mode: "wait" }, shouldShowSuggestions && /* @__PURE__ */ React__default.createElement(
340
+ ), dictation && /* @__PURE__ */ React__default.createElement(DictationCaption, { dictation, hideError: Boolean(errorPlaceholder) }), dictation && /* @__PURE__ */ React__default.createElement(DictationLiveRegion, { dictation }), /* @__PURE__ */ React__default.createElement(AnimatePresence, { mode: "wait" }, shouldShowSuggestions && /* @__PURE__ */ React__default.createElement(
341
341
  motion.div,
342
342
  {
343
343
  variants: dropdownVariants,
@@ -0,0 +1,42 @@
1
+ const joinText = (left, right) => {
2
+ if (!left) return right;
3
+ if (!right) return left;
4
+ if (/\s$/.test(left) || /^\s/.test(right)) return `${left}${right}`;
5
+ return `${left} ${right}`;
6
+ };
7
+ const composeQuery = (prefix, session) => joinText(prefix, session.trim());
8
+ const NON_SPEECH_TAG = /\[[^\]]*\]|\([^)]*\)/g;
9
+ const PUNCTUATION = /\p{P}+/gu;
10
+ const FILLER_PHRASES = ["you know", "i mean", "thank you", "thanks for watching"];
11
+ const FILLER_WORDS = /* @__PURE__ */ new Set([
12
+ "um",
13
+ "umm",
14
+ "uh",
15
+ "uhh",
16
+ "er",
17
+ "erm",
18
+ "ah",
19
+ "hmm",
20
+ "hm",
21
+ "mm",
22
+ "mhm",
23
+ "huh",
24
+ "oh",
25
+ "okay",
26
+ "ok",
27
+ "yeah",
28
+ "so",
29
+ "well",
30
+ "you",
31
+ "bye"
32
+ ]);
33
+ const isMeaningfulTranscript = (text) => {
34
+ const cleaned = text.replace(NON_SPEECH_TAG, " ").replace(PUNCTUATION, " ").toLowerCase().replace(/\s+/g, " ").trim();
35
+ const withoutPhrases = FILLER_PHRASES.reduce(
36
+ (rest, phrase) => rest.replace(new RegExp(`\\b${phrase}\\b`, "g"), " "),
37
+ cleaned
38
+ );
39
+ return withoutPhrases.split(" ").some((word) => word && !FILLER_WORDS.has(word));
40
+ };
41
+
42
+ export { composeQuery, isMeaningfulTranscript };
@@ -0,0 +1,162 @@
1
+ import { useState, useRef, useEffect } from 'react';
2
+ import { useMicrophone, useReducedMotion, useEvent } from '@reactuses/core';
3
+ import { isMeaningfulTranscript, composeQuery } from './transcriptSession.js';
4
+ import { makeRequest } from '../../../lib/fetchClient.js';
5
+
6
+ const MAX_SESSION_MS = 6e4;
7
+ const BAR_COUNT = 16;
8
+ const MIN_LEVEL = 0.12;
9
+ const MIC_BLOCKED = "Allow microphone access in your browser settings, then try again.";
10
+ const UNAVAILABLE = "Voice input is unavailable right now. You can still type.";
11
+ const TRANSCRIBE_FAILED = "Could not transcribe your recording. Try again or type your question.";
12
+ const NO_SPEECH = "We didn't catch a question. Try again or type it.";
13
+ const MIC_CONSTRAINTS = { channelCount: 1 };
14
+ const FLAT_LEVELS = Array.from({ length: BAR_COUNT }, () => MIN_LEVEL);
15
+ const isPermissionError = (error) => {
16
+ const name = error?.name;
17
+ return name === "NotAllowedError" || name === "NotFoundError" || name === "SecurityError";
18
+ };
19
+ const toBarLevel = (level) => Math.min(1, MIN_LEVEL + level * 2.4);
20
+ const fileNameFor = (mediaType) => `dictation.${mediaType.includes("mp4") ? "mp4" : "webm"}`;
21
+ const useSearchDictation = ({
22
+ endpoint,
23
+ headers,
24
+ getText,
25
+ setText,
26
+ onSubmit
27
+ }) => {
28
+ const {
29
+ isSupported,
30
+ level,
31
+ error: micError,
32
+ start: openMic,
33
+ stop: closeMic,
34
+ startRecording,
35
+ stopRecording
36
+ } = useMicrophone({ constraints: MIC_CONSTRAINTS });
37
+ const reducedMotion = useReducedMotion();
38
+ const [status, setStatus] = useState("idle");
39
+ const [error, setError] = useState(null);
40
+ const [errorTone, setErrorTone] = useState(null);
41
+ const [elapsedSeconds, setElapsedSeconds] = useState(0);
42
+ const [levels, setLevels] = useState(FLAT_LEVELS);
43
+ const abortRef = useRef(null);
44
+ const prefixRef = useRef("");
45
+ const fail = useEvent((message, tone = "danger") => {
46
+ setError(message);
47
+ setErrorTone(tone);
48
+ setStatus("error");
49
+ });
50
+ const cancel = useEvent(() => {
51
+ abortRef.current?.abort();
52
+ abortRef.current = null;
53
+ closeMic();
54
+ setText(prefixRef.current);
55
+ setError(null);
56
+ setErrorTone(null);
57
+ setStatus("idle");
58
+ });
59
+ const start = useEvent(async () => {
60
+ const active = status === "connecting" || status === "recording" || status === "transcribing";
61
+ if (!endpoint || active) return;
62
+ const abort = new AbortController();
63
+ abortRef.current = abort;
64
+ prefixRef.current = getText();
65
+ setError(null);
66
+ setErrorTone(null);
67
+ setElapsedSeconds(0);
68
+ setLevels(FLAT_LEVELS);
69
+ setStatus("connecting");
70
+ try {
71
+ if (typeof MediaRecorder === "undefined") throw new Error("MediaRecorder is not supported");
72
+ await openMic();
73
+ if (abort.signal.aborted) {
74
+ closeMic();
75
+ return;
76
+ }
77
+ startRecording();
78
+ setStatus("recording");
79
+ } catch (caught) {
80
+ if (abort.signal.aborted) return;
81
+ closeMic();
82
+ fail(isPermissionError(caught) ? MIC_BLOCKED : UNAVAILABLE);
83
+ }
84
+ });
85
+ const done = useEvent(async () => {
86
+ const abort = abortRef.current;
87
+ if (!endpoint || !abort || status !== "recording") return;
88
+ setStatus("transcribing");
89
+ try {
90
+ const blob = await stopRecording();
91
+ closeMic();
92
+ if (abort.signal.aborted) return;
93
+ if (!blob?.size) return fail(TRANSCRIBE_FAILED);
94
+ const form = new FormData();
95
+ form.append("file", blob, fileNameFor(blob.type));
96
+ const payload = await makeRequest(endpoint, {
97
+ method: "POST",
98
+ headers,
99
+ body: form,
100
+ retry: { maxRetries: 0 },
101
+ signal: abort.signal
102
+ });
103
+ if (abort.signal.aborted) return;
104
+ const text = payload.data?.text?.trim() ?? "";
105
+ if (!isMeaningfulTranscript(text)) return fail(NO_SPEECH, "warning");
106
+ const query = composeQuery(prefixRef.current, text);
107
+ setStatus("idle");
108
+ if (onSubmit) onSubmit(query);
109
+ else setText(query);
110
+ } catch {
111
+ if (!abort.signal.aborted) fail(UNAVAILABLE);
112
+ } finally {
113
+ if (abortRef.current === abort) abortRef.current = null;
114
+ }
115
+ });
116
+ useEffect(() => {
117
+ if (status !== "recording") return;
118
+ const startedAt = Date.now();
119
+ const timer = window.setInterval(() => {
120
+ const elapsed = Date.now() - startedAt;
121
+ setElapsedSeconds(Math.floor(elapsed / 1e3));
122
+ if (elapsed >= MAX_SESSION_MS) void done();
123
+ }, 250);
124
+ return () => window.clearInterval(timer);
125
+ }, [status, done]);
126
+ useEffect(() => {
127
+ if (status !== "recording" || reducedMotion) return;
128
+ setLevels((previous) => [...previous.slice(1), toBarLevel(level)]);
129
+ }, [level, reducedMotion, status]);
130
+ useEffect(() => {
131
+ if (!micError || status !== "recording") return;
132
+ abortRef.current?.abort();
133
+ fail(UNAVAILABLE);
134
+ }, [micError, status, fail]);
135
+ useEffect(() => {
136
+ if (status !== "connecting" && status !== "recording" && status !== "transcribing") return;
137
+ const onKey = (event) => {
138
+ if (event.key === "Escape") {
139
+ event.preventDefault();
140
+ cancel();
141
+ }
142
+ };
143
+ window.addEventListener("keydown", onKey);
144
+ return () => window.removeEventListener("keydown", onKey);
145
+ }, [cancel, status]);
146
+ useEffect(() => () => abortRef.current?.abort(), []);
147
+ return {
148
+ available: Boolean(endpoint) && isSupported,
149
+ status,
150
+ partial: "",
151
+ committed: "",
152
+ error,
153
+ errorTone,
154
+ elapsedSeconds,
155
+ levels,
156
+ start: () => void start(),
157
+ done: () => void done(),
158
+ cancel
159
+ };
160
+ };
161
+
162
+ export { useSearchDictation };
@@ -75,11 +75,13 @@ const MultiSelectDropdown = ({
75
75
  break;
76
76
  }
77
77
  };
78
- return /* @__PURE__ */ React__default.createElement("div", { className: "py-1", role: "listbox", "aria-multiselectable": "true" }, /* @__PURE__ */ React__default.createElement("div", { "aria-live": "polite", "aria-atomic": "true", className: "sr-only" }, announcement), valueOptions.map((option, index) => /* @__PURE__ */ React__default.createElement(
78
+ return /* @__PURE__ */ React__default.createElement("div", { className: "flex flex-col min-h-0 flex-1", role: "listbox", "aria-multiselectable": "true" }, /* @__PURE__ */ React__default.createElement("div", { "aria-live": "polite", "aria-atomic": "true", className: "sr-only" }, announcement), /* @__PURE__ */ React__default.createElement("div", { className: "max-h-60 overflow-y-scroll py-1" }, valueOptions.map((option, index) => /* @__PURE__ */ React__default.createElement(
79
79
  "button",
80
80
  {
81
81
  key: option.id,
82
- ref: (el) => buttonRefs.current[index] = el,
82
+ ref: (el) => {
83
+ buttonRefs.current[index] = el;
84
+ },
83
85
  type: "button",
84
86
  className: "relative flex w-full text-left cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground",
85
87
  onClick: () => handleValueToggle(option.id),
@@ -90,7 +92,7 @@ const MultiSelectDropdown = ({
90
92
  isValueSelected(option.id) && /* @__PURE__ */ React__default.createElement(Check, { size: 14, className: "mr-2" }),
91
93
  !isValueSelected(option.id) && /* @__PURE__ */ React__default.createElement("span", { className: "mr-2 w-3.5" }),
92
94
  option.label
93
- )), /* @__PURE__ */ React__default.createElement("div", { className: "border-t border-border mt-1 pt-2 px-2 pb-2" }, /* @__PURE__ */ React__default.createElement(
95
+ ))), /* @__PURE__ */ React__default.createElement("div", { className: "border-t border-border pt-2 px-2 pb-2 shrink-0" }, /* @__PURE__ */ React__default.createElement(
94
96
  Button,
95
97
  {
96
98
  ref: applyButtonRef,
@@ -209,7 +209,7 @@ const Filter = forwardRef(
209
209
  onOpenChange: (open) => setOpenDropdownType(open ? "value" : null)
210
210
  },
211
211
  /* @__PURE__ */ React__default.createElement(PopoverTrigger, { asChild: true }, /* @__PURE__ */ React__default.createElement(Button, { variant: "ghost", size: "xs", className: "font-bold" }, getDisplayValue())),
212
- /* @__PURE__ */ React__default.createElement(PopoverContent, { align: "start", className: "w-64 max-h-[min(320px,50vh)] overflow-y-auto p-0" }, inputType === "custom" && /* @__PURE__ */ React__default.createElement("div", { className: "p-3" }, /* @__PURE__ */ React__default.createElement(
212
+ /* @__PURE__ */ React__default.createElement(PopoverContent, { align: "start", className: "w-64 max-h-[min(320px,50vh)] overflow-hidden p-0 flex flex-col" }, inputType === "custom" && /* @__PURE__ */ React__default.createElement("div", { className: "p-3" }, /* @__PURE__ */ React__default.createElement(
213
213
  CustomValueInput,
214
214
  {
215
215
  inputValue,
@@ -219,7 +219,7 @@ const Filter = forwardRef(
219
219
  onSubmit: handleCustomValueSubmit,
220
220
  handleKeyDown
221
221
  }
222
- )), inputType === "dropdown" && /* @__PURE__ */ React__default.createElement(React__default.Fragment, null, internalFilter.source ? /* @__PURE__ */ React__default.createElement(React__default.Fragment, null, isMultiSelect ? /* @__PURE__ */ React__default.createElement(
222
+ )), inputType === "dropdown" && /* @__PURE__ */ React__default.createElement("div", { className: "flex flex-col min-h-0 flex-1" }, internalFilter.source ? /* @__PURE__ */ React__default.createElement(React__default.Fragment, null, isMultiSelect ? /* @__PURE__ */ React__default.createElement(
223
223
  MultiSelectDropdown,
224
224
  {
225
225
  internalFilter,
@@ -227,14 +227,14 @@ const Filter = forwardRef(
227
227
  onConfirm: handleMultiSelectConfirm,
228
228
  onClose: () => setOpenDropdownType(null)
229
229
  }
230
- ) : /* @__PURE__ */ React__default.createElement(
230
+ ) : /* @__PURE__ */ React__default.createElement("div", { className: "overflow-y-auto" }, /* @__PURE__ */ React__default.createElement(
231
231
  SingleSelectDropdown,
232
232
  {
233
233
  valueOptions,
234
234
  isValueSelected,
235
235
  onValueSelect: handleValueToggle
236
236
  }
237
- )) : /* @__PURE__ */ React__default.createElement("div", { className: "p-3 text-sm text-gray-500" }, sourceFirstMessage)))
237
+ ))) : /* @__PURE__ */ React__default.createElement("div", { className: "p-3 text-sm text-gray-500" }, sourceFirstMessage)))
238
238
  ),
239
239
  /* @__PURE__ */ React__default.createElement(
240
240
  Button,
@@ -1442,6 +1442,9 @@ video {
1442
1442
  .h-px{
1443
1443
  height: 1px;
1444
1444
  }
1445
+ .max-h-60{
1446
+ max-height: 15rem;
1447
+ }
1445
1448
  .max-h-\[300px\]{
1446
1449
  max-height: 300px;
1447
1450
  }
@@ -1472,6 +1475,9 @@ video {
1472
1475
  .min-h-11{
1473
1476
  min-height: 2.75rem;
1474
1477
  }
1478
+ .min-h-16{
1479
+ min-height: 4rem;
1480
+ }
1475
1481
  .min-h-20{
1476
1482
  min-height: 5rem;
1477
1483
  }
@@ -1505,6 +1511,9 @@ video {
1505
1511
  .w-0{
1506
1512
  width: 0px;
1507
1513
  }
1514
+ .w-0\.5{
1515
+ width: 0.125rem;
1516
+ }
1508
1517
  .w-1{
1509
1518
  width: 0.25rem;
1510
1519
  }
@@ -1936,6 +1945,9 @@ video {
1936
1945
  .gap-\[2px\]{
1937
1946
  gap: 2px;
1938
1947
  }
1948
+ .gap-px{
1949
+ gap: 1px;
1950
+ }
1939
1951
  .-space-x-2 > :not([hidden]) ~ :not([hidden]){
1940
1952
  --tw-space-x-reverse: 0;
1941
1953
  margin-right: calc(-0.5rem * var(--tw-space-x-reverse));
@@ -2027,6 +2039,9 @@ video {
2027
2039
  .overflow-x-hidden{
2028
2040
  overflow-x: hidden;
2029
2041
  }
2042
+ .overflow-y-scroll{
2043
+ overflow-y: scroll;
2044
+ }
2030
2045
  .truncate{
2031
2046
  overflow: hidden;
2032
2047
  text-overflow: ellipsis;
@@ -2087,6 +2102,10 @@ video {
2087
2102
  border-bottom-right-radius: 0.25rem;
2088
2103
  border-bottom-left-radius: 0.25rem;
2089
2104
  }
2105
+ .rounded-b-\[4px\]{
2106
+ border-bottom-right-radius: 4px;
2107
+ border-bottom-left-radius: 4px;
2108
+ }
2090
2109
  .rounded-b-lg{
2091
2110
  border-bottom-right-radius: 0.5rem;
2092
2111
  border-bottom-left-radius: 0.5rem;
@@ -2115,6 +2134,10 @@ video {
2115
2134
  border-top-left-radius: 1rem;
2116
2135
  border-top-right-radius: 1rem;
2117
2136
  }
2137
+ .rounded-t-\[4px\]{
2138
+ border-top-left-radius: 4px;
2139
+ border-top-right-radius: 4px;
2140
+ }
2118
2141
  .rounded-t-lg{
2119
2142
  border-top-left-radius: 0.5rem;
2120
2143
  border-top-right-radius: 0.5rem;
@@ -2123,6 +2146,9 @@ video {
2123
2146
  border-top-left-radius: 0.375rem;
2124
2147
  border-top-right-radius: 0.375rem;
2125
2148
  }
2149
+ .\!border-2{
2150
+ border-width: 2px !important;
2151
+ }
2126
2152
  .border{
2127
2153
  border-width: 1px;
2128
2154
  }
@@ -2386,6 +2412,9 @@ video {
2386
2412
  .bg-primary{
2387
2413
  background-color: var(--bfc-primary);
2388
2414
  }
2415
+ .bg-red{
2416
+ background-color: var(--bfc-v2-color-red-700, #c9372c);
2417
+ }
2389
2418
  .bg-red-100{
2390
2419
  background-color: var(--bfc-v2-color-red-100, #ffeceb);
2391
2420
  }
@@ -3074,11 +3103,6 @@ video {
3074
3103
  .outline-lime-200{
3075
3104
  outline-color: var(--bfc-v2-color-lime-200, #d6f589);
3076
3105
  }
3077
- .\!ring-0{
3078
- --tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color) !important;
3079
- --tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(0px + var(--tw-ring-offset-width)) var(--tw-ring-color) !important;
3080
- box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000) !important;
3081
- }
3082
3106
  .ring-0{
3083
3107
  --tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);
3084
3108
  --tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(0px + var(--tw-ring-offset-width)) var(--tw-ring-color);
@@ -3244,6 +3268,9 @@ video {
3244
3268
  .ease-out{
3245
3269
  animation-timing-function: cubic-bezier(0, 0, 0.2, 1);
3246
3270
  }
3271
+ .running{
3272
+ animation-play-state: running;
3273
+ }
3247
3274
  .\@container{
3248
3275
  container-type: inline-size;
3249
3276
  }
@@ -3325,6 +3352,14 @@ video {
3325
3352
  pointer-events: none;
3326
3353
  border-radius: inherit;
3327
3354
  z-index: 0;
3355
+ }
3356
+ .bg-yellowfin-gradient{
3357
+ border: 1.2px solid transparent;
3358
+ background: linear-gradient(145.85deg, #1868DB 7.35%, #357DE8 23.69%, #7EE2B8 61.42%, #B4EE2B 82.59%),
3359
+ linear-gradient(145.85deg, rgba(180, 238, 43, 0.7) 3.02%, rgba(126, 226, 184, 0.7) 24.19%, rgba(53, 125, 232, 0.7) 61.92%, rgba(24, 104, 219, 0.7) 78.26%);
3360
+ transition: all 0.2s ease-in-out;
3361
+ background-origin: border-box;
3362
+ background-clip: content-box, border-box;
3328
3363
  }
3329
3364
  .shimmer {
3330
3365
  animation: shimmer 3s infinite linear;
@@ -3582,14 +3617,12 @@ strong {
3582
3617
  color: var(--bfc-foreground);
3583
3618
  }
3584
3619
 
3585
- .placeholder\:text-gray-400::-moz-placeholder{
3586
- --tw-text-opacity: 1;
3587
- color: rgb(156 163 175 / var(--tw-text-opacity, 1));
3620
+ .placeholder\:text-destructive::-moz-placeholder{
3621
+ color: var(--bfc-destructive-text);
3588
3622
  }
3589
3623
 
3590
- .placeholder\:text-gray-400::placeholder{
3591
- --tw-text-opacity: 1;
3592
- color: rgb(156 163 175 / var(--tw-text-opacity, 1));
3624
+ .placeholder\:text-destructive::placeholder{
3625
+ color: var(--bfc-destructive-text);
3593
3626
  }
3594
3627
 
3595
3628
  .placeholder\:text-muted-foreground::-moz-placeholder{
@@ -3600,6 +3633,14 @@ strong {
3600
3633
  color: var(--bfc-muted-foreground);
3601
3634
  }
3602
3635
 
3636
+ .placeholder\:text-warning::-moz-placeholder{
3637
+ color: var(--bfc-warning-text);
3638
+ }
3639
+
3640
+ .placeholder\:text-warning::placeholder{
3641
+ color: var(--bfc-warning-text);
3642
+ }
3643
+
3603
3644
  .after\:absolute::after{
3604
3645
  content: var(--tw-content);
3605
3646
  position: absolute;
@@ -1,5 +1,6 @@
1
1
  const Features = {
2
- HistoryTab: "enableHistoryTab"
2
+ HistoryTab: "enableHistoryTab",
3
+ TranscribeEnabled: "help-center.transcribe.enabled"
3
4
  };
4
5
 
5
6
  export { Features };
@@ -18,6 +18,11 @@ let globalFallbackConfig = { ...defaultFallbackConfig };
18
18
  async function delay(ms) {
19
19
  return new Promise((resolve) => setTimeout(resolve, ms));
20
20
  }
21
+ function withoutContentType(headers) {
22
+ const result = new Headers(headers);
23
+ result.delete("Content-Type");
24
+ return result;
25
+ }
21
26
  async function makeRequest(url, config = {}) {
22
27
  const { retry = {}, headers = {}, body, isStream = false, fallback = {}, ...fetchConfig } = config;
23
28
  const retryOptions = {
@@ -29,15 +34,16 @@ async function makeRequest(url, config = {}) {
29
34
  ...fallback
30
35
  };
31
36
  let attempt = 1;
37
+ const isFormData = typeof FormData !== "undefined" && body instanceof FormData;
32
38
  while (true) {
33
39
  try {
34
40
  const response = await fetch(url, {
35
41
  ...fetchConfig,
36
- headers: {
42
+ headers: isFormData ? withoutContentType(headers) : {
37
43
  "Content-Type": "application/json",
38
44
  ...headers
39
45
  },
40
- body: body ? JSON.stringify(body) : void 0
46
+ body: isFormData ? body : body ? JSON.stringify(body) : void 0
41
47
  });
42
48
  if (response.ok) {
43
49
  if (isStream) {
@@ -1,4 +1,5 @@
1
1
  export declare const Features: {
2
2
  readonly UseSseForAnswers: "use-sse-for-answers";
3
3
  readonly HistoryTab: "enableHistoryTab";
4
+ readonly TranscribeEnabled: "help-center.transcribe.enabled";
4
5
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@brainfish-ai/components",
3
- "version": "0.36.0",
3
+ "version": "0.37.0",
4
4
  "description": "Brainfish Components",
5
5
  "license": "MIT",
6
6
  "main": "./dist/esm/index.js",
@@ -185,6 +185,7 @@
185
185
  "@radix-ui/react-switch": "^1.1.2",
186
186
  "@radix-ui/react-tabs": "^1.1.21",
187
187
  "@radix-ui/react-tooltip": "^1.2.8",
188
+ "@reactuses/core": "^6.6.1",
188
189
  "@tanstack/react-table": "^8.21.3",
189
190
  "ajv": "^8.20.0",
190
191
  "ajv-errors": "^3.0.0",
@@ -224,6 +225,7 @@
224
225
  "y-prosemirror": "^1.3.7",
225
226
  "y-protocols": "^1.0.6",
226
227
  "y-webrtc": "^10.3.0",
227
- "yjs": "^13.6.27"
228
+ "yjs": "^13.6.27",
229
+ "zod": "^3.25.76"
228
230
  }
229
231
  }