@brainfish-ai/components 0.36.1 → 0.37.1
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 +65 -6
- 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/global.css +46 -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
|
)
|
|
@@ -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 };
|
package/dist/esm/global.css
CHANGED
|
@@ -1475,6 +1475,9 @@ video {
|
|
|
1475
1475
|
.min-h-11{
|
|
1476
1476
|
min-height: 2.75rem;
|
|
1477
1477
|
}
|
|
1478
|
+
.min-h-16{
|
|
1479
|
+
min-height: 4rem;
|
|
1480
|
+
}
|
|
1478
1481
|
.min-h-20{
|
|
1479
1482
|
min-height: 5rem;
|
|
1480
1483
|
}
|
|
@@ -1508,6 +1511,9 @@ video {
|
|
|
1508
1511
|
.w-0{
|
|
1509
1512
|
width: 0px;
|
|
1510
1513
|
}
|
|
1514
|
+
.w-0\.5{
|
|
1515
|
+
width: 0.125rem;
|
|
1516
|
+
}
|
|
1511
1517
|
.w-1{
|
|
1512
1518
|
width: 0.25rem;
|
|
1513
1519
|
}
|
|
@@ -1939,6 +1945,9 @@ video {
|
|
|
1939
1945
|
.gap-\[2px\]{
|
|
1940
1946
|
gap: 2px;
|
|
1941
1947
|
}
|
|
1948
|
+
.gap-px{
|
|
1949
|
+
gap: 1px;
|
|
1950
|
+
}
|
|
1942
1951
|
.-space-x-2 > :not([hidden]) ~ :not([hidden]){
|
|
1943
1952
|
--tw-space-x-reverse: 0;
|
|
1944
1953
|
margin-right: calc(-0.5rem * var(--tw-space-x-reverse));
|
|
@@ -2093,6 +2102,10 @@ video {
|
|
|
2093
2102
|
border-bottom-right-radius: 0.25rem;
|
|
2094
2103
|
border-bottom-left-radius: 0.25rem;
|
|
2095
2104
|
}
|
|
2105
|
+
.rounded-b-\[4px\]{
|
|
2106
|
+
border-bottom-right-radius: 4px;
|
|
2107
|
+
border-bottom-left-radius: 4px;
|
|
2108
|
+
}
|
|
2096
2109
|
.rounded-b-lg{
|
|
2097
2110
|
border-bottom-right-radius: 0.5rem;
|
|
2098
2111
|
border-bottom-left-radius: 0.5rem;
|
|
@@ -2121,6 +2134,10 @@ video {
|
|
|
2121
2134
|
border-top-left-radius: 1rem;
|
|
2122
2135
|
border-top-right-radius: 1rem;
|
|
2123
2136
|
}
|
|
2137
|
+
.rounded-t-\[4px\]{
|
|
2138
|
+
border-top-left-radius: 4px;
|
|
2139
|
+
border-top-right-radius: 4px;
|
|
2140
|
+
}
|
|
2124
2141
|
.rounded-t-lg{
|
|
2125
2142
|
border-top-left-radius: 0.5rem;
|
|
2126
2143
|
border-top-right-radius: 0.5rem;
|
|
@@ -2129,6 +2146,9 @@ video {
|
|
|
2129
2146
|
border-top-left-radius: 0.375rem;
|
|
2130
2147
|
border-top-right-radius: 0.375rem;
|
|
2131
2148
|
}
|
|
2149
|
+
.\!border-2{
|
|
2150
|
+
border-width: 2px !important;
|
|
2151
|
+
}
|
|
2132
2152
|
.border{
|
|
2133
2153
|
border-width: 1px;
|
|
2134
2154
|
}
|
|
@@ -2392,6 +2412,9 @@ video {
|
|
|
2392
2412
|
.bg-primary{
|
|
2393
2413
|
background-color: var(--bfc-primary);
|
|
2394
2414
|
}
|
|
2415
|
+
.bg-red{
|
|
2416
|
+
background-color: var(--bfc-v2-color-red-700, #c9372c);
|
|
2417
|
+
}
|
|
2395
2418
|
.bg-red-100{
|
|
2396
2419
|
background-color: var(--bfc-v2-color-red-100, #ffeceb);
|
|
2397
2420
|
}
|
|
@@ -3080,11 +3103,6 @@ video {
|
|
|
3080
3103
|
.outline-lime-200{
|
|
3081
3104
|
outline-color: var(--bfc-v2-color-lime-200, #d6f589);
|
|
3082
3105
|
}
|
|
3083
|
-
.\!ring-0{
|
|
3084
|
-
--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color) !important;
|
|
3085
|
-
--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(0px + var(--tw-ring-offset-width)) var(--tw-ring-color) !important;
|
|
3086
|
-
box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000) !important;
|
|
3087
|
-
}
|
|
3088
3106
|
.ring-0{
|
|
3089
3107
|
--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);
|
|
3090
3108
|
--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(0px + var(--tw-ring-offset-width)) var(--tw-ring-color);
|
|
@@ -3250,6 +3268,9 @@ video {
|
|
|
3250
3268
|
.ease-out{
|
|
3251
3269
|
animation-timing-function: cubic-bezier(0, 0, 0.2, 1);
|
|
3252
3270
|
}
|
|
3271
|
+
.running{
|
|
3272
|
+
animation-play-state: running;
|
|
3273
|
+
}
|
|
3253
3274
|
.\@container{
|
|
3254
3275
|
container-type: inline-size;
|
|
3255
3276
|
}
|
|
@@ -3331,6 +3352,14 @@ video {
|
|
|
3331
3352
|
pointer-events: none;
|
|
3332
3353
|
border-radius: inherit;
|
|
3333
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;
|
|
3334
3363
|
}
|
|
3335
3364
|
.shimmer {
|
|
3336
3365
|
animation: shimmer 3s infinite linear;
|
|
@@ -3588,14 +3617,12 @@ strong {
|
|
|
3588
3617
|
color: var(--bfc-foreground);
|
|
3589
3618
|
}
|
|
3590
3619
|
|
|
3591
|
-
.placeholder\:text-
|
|
3592
|
-
--
|
|
3593
|
-
color: rgb(156 163 175 / var(--tw-text-opacity, 1));
|
|
3620
|
+
.placeholder\:text-destructive::-moz-placeholder{
|
|
3621
|
+
color: var(--bfc-destructive-text);
|
|
3594
3622
|
}
|
|
3595
3623
|
|
|
3596
|
-
.placeholder\:text-
|
|
3597
|
-
--
|
|
3598
|
-
color: rgb(156 163 175 / var(--tw-text-opacity, 1));
|
|
3624
|
+
.placeholder\:text-destructive::placeholder{
|
|
3625
|
+
color: var(--bfc-destructive-text);
|
|
3599
3626
|
}
|
|
3600
3627
|
|
|
3601
3628
|
.placeholder\:text-muted-foreground::-moz-placeholder{
|
|
@@ -3606,6 +3633,14 @@ strong {
|
|
|
3606
3633
|
color: var(--bfc-muted-foreground);
|
|
3607
3634
|
}
|
|
3608
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
|
+
|
|
3609
3644
|
.after\:absolute::after{
|
|
3610
3645
|
content: var(--tw-content);
|
|
3611
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.1",
|
|
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
|
}
|