@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.
- package/dist/components/chat-search/DictationChrome.d.ts +20 -0
- package/dist/components/chat-search/FollowUpSearchBar.d.ts +2 -0
- package/dist/components/chat-search/PrimarySearch.d.ts +2 -0
- package/dist/components/chat-search/hooks/index.d.ts +1 -0
- package/dist/components/chat-search/hooks/transcriptSession.d.ts +6 -0
- package/dist/components/chat-search/hooks/useSearchDictation.d.ts +25 -0
- package/dist/components/chat-search/types.d.ts +2 -0
- package/dist/esm/components/chat-search/ChatSearch.js +66 -7
- package/dist/esm/components/chat-search/DictationChrome.js +68 -0
- package/dist/esm/components/chat-search/FollowUpSearchBar.js +30 -9
- package/dist/esm/components/chat-search/PrimarySearch.js +22 -22
- package/dist/esm/components/chat-search/hooks/transcriptSession.js +42 -0
- package/dist/esm/components/chat-search/hooks/useSearchDictation.js +162 -0
- package/dist/esm/components/filter/components/MultiSelectDropdown.js +5 -3
- package/dist/esm/components/filter/filter.js +4 -4
- package/dist/esm/global.css +52 -11
- package/dist/esm/lib/features.js +2 -1
- package/dist/esm/lib/fetchClient.js +8 -2
- package/dist/lib/features.d.ts +1 -0
- package/package.json +4 -2
|
@@ -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;
|
|
@@ -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
|
-
|
|
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
|
-
|
|
767
|
-
|
|
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 {
|
|
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-
|
|
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
|
|
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:
|
|
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
|
|
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
|
|
203
|
-
|
|
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-
|
|
229
|
-
|
|
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
|
-
|
|
234
|
-
|
|
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
|
|
242
|
-
"
|
|
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: "
|
|
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) =>
|
|
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
|
|
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-
|
|
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(
|
|
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,
|
package/dist/esm/global.css
CHANGED
|
@@ -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-
|
|
3586
|
-
--
|
|
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-
|
|
3591
|
-
--
|
|
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;
|
package/dist/esm/lib/features.js
CHANGED
|
@@ -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) {
|
package/dist/lib/features.d.ts
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@brainfish-ai/components",
|
|
3
|
-
"version": "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
|
}
|