@smartspace/chat-ui 1.14.4-dev.e6584f0 → 1.14.4-dev.f08c0a0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.css +1 -1
- package/dist/index.css.map +1 -1
- package/dist/index.d.ts +142 -1
- package/dist/index.js +595 -22
- package/dist/index.js.map +1 -1
- package/package.json +4 -1
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import MuiButton from '@mui/material/Button';
|
|
2
|
-
import
|
|
3
|
-
import { Loader2, ChevronDown, Check, Cpu, SlidersHorizontal, X, Globe, Zap,
|
|
2
|
+
import IconButton2 from '@mui/material/IconButton';
|
|
3
|
+
import { Loader2, ChevronDown, Check, Cpu, SlidersHorizontal, X, Globe, Zap, Square, Mic, Paperclip, Send, Minimize2, AlertTriangle, FileImage, FileVideo, FileAudio, FileArchive, FileCode, FileSpreadsheet, Presentation, FileText, ChevronUp, ExternalLink, Download, ShieldAlert, Copy } from 'lucide-react';
|
|
4
4
|
import * as React9 from 'react';
|
|
5
5
|
import React9__default, { createContext, forwardRef, useImperativeHandle, useRef, useState, useEffect, useMemo, useCallback, useContext, useSyncExternalStore } from 'react';
|
|
6
6
|
import { createPortal } from 'react-dom';
|
|
@@ -17,7 +17,7 @@ import { codeBlockSchema, commonmark } from '@milkdown/preset-commonmark';
|
|
|
17
17
|
import { Slice, NodeType } from '@milkdown/prose/model';
|
|
18
18
|
import { MilkdownProvider, useEditor, Milkdown } from '@milkdown/react';
|
|
19
19
|
import { PluginKey, Plugin } from '@milkdown/prose/state';
|
|
20
|
-
import {
|
|
20
|
+
import { DecorationSet, Decoration } from '@milkdown/prose/view';
|
|
21
21
|
import 'crypto';
|
|
22
22
|
import '@milkdown/prose';
|
|
23
23
|
import '@milkdown/prose/inputrules';
|
|
@@ -842,6 +842,37 @@ function useMessages(threadId) {
|
|
|
842
842
|
});
|
|
843
843
|
}
|
|
844
844
|
|
|
845
|
+
// src/domains/speech/queryKeys.ts
|
|
846
|
+
var speechKeys = {
|
|
847
|
+
all: ["speech"],
|
|
848
|
+
config: () => [...speechKeys.all, "config"]
|
|
849
|
+
};
|
|
850
|
+
|
|
851
|
+
// src/domains/speech/queries.ts
|
|
852
|
+
var unavailable = { enabled: false, defaultLocale: "en-US" };
|
|
853
|
+
var isNotFound = (error) => error?.type === "NotFound" || error?.response?.status === 404;
|
|
854
|
+
function useSpeechConfig() {
|
|
855
|
+
const service = useChatService();
|
|
856
|
+
const supported = !!service.getSpeechConfig && !!service.getSpeechToken;
|
|
857
|
+
return useQuery({
|
|
858
|
+
...queryOptions({
|
|
859
|
+
queryKey: speechKeys.config(),
|
|
860
|
+
queryFn: async () => {
|
|
861
|
+
try {
|
|
862
|
+
return await service.getSpeechConfig?.() ?? unavailable;
|
|
863
|
+
} catch (error) {
|
|
864
|
+
if (isNotFound(error)) return unavailable;
|
|
865
|
+
throw error;
|
|
866
|
+
}
|
|
867
|
+
},
|
|
868
|
+
staleTime: Infinity,
|
|
869
|
+
retry: (failureCount) => failureCount < 2,
|
|
870
|
+
retryDelay: 1e3
|
|
871
|
+
}),
|
|
872
|
+
enabled: supported
|
|
873
|
+
});
|
|
874
|
+
}
|
|
875
|
+
|
|
845
876
|
// ../../node_modules/.pnpm/@milkdown+exception@7.20.0/node_modules/@milkdown/exception/lib/index.js
|
|
846
877
|
var ErrorCode = /* @__PURE__ */ (function(ErrorCode2) {
|
|
847
878
|
ErrorCode2["docTypeError"] = "docTypeError";
|
|
@@ -975,6 +1006,75 @@ var autolink = $prose(() => {
|
|
|
975
1006
|
}
|
|
976
1007
|
});
|
|
977
1008
|
});
|
|
1009
|
+
var dictationGhostKey = new PluginKey("dictationGhost");
|
|
1010
|
+
function buildDecorations2(state, text6) {
|
|
1011
|
+
const doc = state.doc;
|
|
1012
|
+
const from = state.selection.head;
|
|
1013
|
+
const before = from > 0 ? doc.textBetween(from - 1, from, " ") : "";
|
|
1014
|
+
const display = before !== "" && !/\s/.test(before) ? ` ${text6}` : text6;
|
|
1015
|
+
const widget = Decoration.widget(
|
|
1016
|
+
from,
|
|
1017
|
+
() => {
|
|
1018
|
+
const span = document.createElement("span");
|
|
1019
|
+
span.className = "md-editor__dictation-ghost";
|
|
1020
|
+
span.setAttribute("aria-hidden", "true");
|
|
1021
|
+
span.appendChild(document.createTextNode(display));
|
|
1022
|
+
const dots = document.createElement("span");
|
|
1023
|
+
dots.className = "md-editor__dictation-dots";
|
|
1024
|
+
for (let i = 0; i < 3; i += 1)
|
|
1025
|
+
dots.appendChild(document.createElement("span"));
|
|
1026
|
+
span.appendChild(dots);
|
|
1027
|
+
return span;
|
|
1028
|
+
},
|
|
1029
|
+
{
|
|
1030
|
+
// Render after the caret so typing still lands where the user expects.
|
|
1031
|
+
side: 1,
|
|
1032
|
+
// Keyed by content so ProseMirror reuses the node between renders instead
|
|
1033
|
+
// of tearing it down on every interim result.
|
|
1034
|
+
key: `dictation-ghost:${display}`
|
|
1035
|
+
}
|
|
1036
|
+
);
|
|
1037
|
+
const decorations = [widget];
|
|
1038
|
+
const $head = state.selection.$head;
|
|
1039
|
+
if ($head.depth > 0) {
|
|
1040
|
+
decorations.push(
|
|
1041
|
+
Decoration.node($head.before(), $head.after(), {
|
|
1042
|
+
class: "md-editor__has-dictation-ghost"
|
|
1043
|
+
})
|
|
1044
|
+
);
|
|
1045
|
+
}
|
|
1046
|
+
return DecorationSet.create(doc, decorations);
|
|
1047
|
+
}
|
|
1048
|
+
var dictationGhost = $prose(() => {
|
|
1049
|
+
return new Plugin({
|
|
1050
|
+
key: dictationGhostKey,
|
|
1051
|
+
state: {
|
|
1052
|
+
init: () => "",
|
|
1053
|
+
apply(tr, previous2) {
|
|
1054
|
+
const meta = tr.getMeta(dictationGhostKey);
|
|
1055
|
+
if (meta) return meta.text;
|
|
1056
|
+
if (tr.docChanged || tr.selectionSet) return "";
|
|
1057
|
+
return previous2;
|
|
1058
|
+
}
|
|
1059
|
+
},
|
|
1060
|
+
props: {
|
|
1061
|
+
decorations(state) {
|
|
1062
|
+
const text6 = dictationGhostKey.getState(state) ?? "";
|
|
1063
|
+
if (!text6) return DecorationSet.empty;
|
|
1064
|
+
return buildDecorations2(state, text6);
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
});
|
|
1068
|
+
});
|
|
1069
|
+
function setDictationGhost(view, text6) {
|
|
1070
|
+
const current = dictationGhostKey.getState(view.state) ?? "";
|
|
1071
|
+
if (current === text6) return;
|
|
1072
|
+
const tr = view.state.tr.setMeta(dictationGhostKey, {
|
|
1073
|
+
text: text6
|
|
1074
|
+
});
|
|
1075
|
+
tr.setMeta("addToHistory", false);
|
|
1076
|
+
view.dispatch(tr);
|
|
1077
|
+
}
|
|
978
1078
|
|
|
979
1079
|
// src/shared/markdown/extensions/fileTag.ts
|
|
980
1080
|
var fileTag = $node("fileTag", () => ({
|
|
@@ -1884,7 +1984,7 @@ function EditorInner({
|
|
|
1884
1984
|
} catch {
|
|
1885
1985
|
}
|
|
1886
1986
|
});
|
|
1887
|
-
}).use(commonmark).use(history).use(clipboard).use(listener).use(autolink).use(fileTag).use(ssImageNode).use(ssImageView).use(htmlPreviewView).use(enableMentions ? mention : fileTag);
|
|
1987
|
+
}).use(commonmark).use(history).use(clipboard).use(listener).use(autolink).use(dictationGhost).use(fileTag).use(ssImageNode).use(ssImageView).use(htmlPreviewView).use(enableMentions ? mention : fileTag);
|
|
1888
1988
|
},
|
|
1889
1989
|
[isEditable]
|
|
1890
1990
|
);
|
|
@@ -2094,6 +2194,25 @@ function EditorInner({
|
|
|
2094
2194
|
return value ?? "";
|
|
2095
2195
|
}
|
|
2096
2196
|
},
|
|
2197
|
+
insertText: (text6) => {
|
|
2198
|
+
const view = viewRef.current;
|
|
2199
|
+
if (!view || !isEditable || !text6) return;
|
|
2200
|
+
try {
|
|
2201
|
+
const { from } = view.state.selection;
|
|
2202
|
+
const before = from > 0 ? view.state.doc.textBetween(from - 1, from, " ") : "";
|
|
2203
|
+
const needsSpace = before !== "" && !/\s/.test(before);
|
|
2204
|
+
insertTextAtSelection(view, (needsSpace ? " " : "") + text6);
|
|
2205
|
+
} catch {
|
|
2206
|
+
}
|
|
2207
|
+
},
|
|
2208
|
+
setDictationGhost: (text6) => {
|
|
2209
|
+
const view = viewRef.current;
|
|
2210
|
+
if (!view || !isEditable) return;
|
|
2211
|
+
try {
|
|
2212
|
+
setDictationGhost(view, text6);
|
|
2213
|
+
} catch {
|
|
2214
|
+
}
|
|
2215
|
+
},
|
|
2097
2216
|
clear: () => {
|
|
2098
2217
|
const view = viewRef.current;
|
|
2099
2218
|
if (!view) return;
|
|
@@ -2437,6 +2556,326 @@ var MarkdownEditor = forwardRef((props, ref) => {
|
|
|
2437
2556
|
return /* @__PURE__ */ jsx(MilkdownProvider, { children: /* @__PURE__ */ jsx(EditorInner, { ...props, editorHandleRef: ref }) });
|
|
2438
2557
|
});
|
|
2439
2558
|
MarkdownEditor.displayName = "MarkdownEditor";
|
|
2559
|
+
var TOKEN_EXPIRY_MARGIN_MS = 15e3;
|
|
2560
|
+
var noLevelMonitor = () => void 0;
|
|
2561
|
+
var SILENCE_HINT_MS = 6e3;
|
|
2562
|
+
var SILENCE_LEVEL = 3;
|
|
2563
|
+
function monitorInputLevel(stream, onSilent, onSound) {
|
|
2564
|
+
let context;
|
|
2565
|
+
let source;
|
|
2566
|
+
let analyser;
|
|
2567
|
+
try {
|
|
2568
|
+
context = new AudioContext();
|
|
2569
|
+
source = context.createMediaStreamSource(stream);
|
|
2570
|
+
analyser = context.createAnalyser();
|
|
2571
|
+
analyser.fftSize = 512;
|
|
2572
|
+
source.connect(analyser);
|
|
2573
|
+
} catch {
|
|
2574
|
+
return () => void 0;
|
|
2575
|
+
}
|
|
2576
|
+
if (context.state === "suspended")
|
|
2577
|
+
void context.resume().catch(() => void 0);
|
|
2578
|
+
const samples = new Uint8Array(analyser.fftSize);
|
|
2579
|
+
let lastSound = Date.now();
|
|
2580
|
+
let silent = false;
|
|
2581
|
+
let stopped = false;
|
|
2582
|
+
const poll = window.setInterval(() => {
|
|
2583
|
+
if (context.state !== "running") return;
|
|
2584
|
+
analyser.getByteTimeDomainData(samples);
|
|
2585
|
+
let peak = 0;
|
|
2586
|
+
for (const sample of samples) peak = Math.max(peak, Math.abs(sample - 128));
|
|
2587
|
+
if (peak > SILENCE_LEVEL) {
|
|
2588
|
+
lastSound = Date.now();
|
|
2589
|
+
if (silent) {
|
|
2590
|
+
silent = false;
|
|
2591
|
+
onSound();
|
|
2592
|
+
}
|
|
2593
|
+
} else if (!silent && Date.now() - lastSound > SILENCE_HINT_MS) {
|
|
2594
|
+
silent = true;
|
|
2595
|
+
onSilent();
|
|
2596
|
+
}
|
|
2597
|
+
}, 250);
|
|
2598
|
+
return () => {
|
|
2599
|
+
if (stopped) return;
|
|
2600
|
+
stopped = true;
|
|
2601
|
+
window.clearInterval(poll);
|
|
2602
|
+
try {
|
|
2603
|
+
source.disconnect();
|
|
2604
|
+
void context.close().catch(() => void 0);
|
|
2605
|
+
} catch {
|
|
2606
|
+
}
|
|
2607
|
+
};
|
|
2608
|
+
}
|
|
2609
|
+
var STALE_TOKEN_FLOOR_MS = 6e4;
|
|
2610
|
+
function kickOff(fn) {
|
|
2611
|
+
let promise;
|
|
2612
|
+
try {
|
|
2613
|
+
promise = Promise.resolve(fn());
|
|
2614
|
+
} catch (e) {
|
|
2615
|
+
promise = Promise.reject(e);
|
|
2616
|
+
}
|
|
2617
|
+
promise.catch(() => void 0);
|
|
2618
|
+
return promise;
|
|
2619
|
+
}
|
|
2620
|
+
var isSupported = () => typeof window !== "undefined" && window.isSecureContext && !!navigator.mediaDevices?.getUserMedia;
|
|
2621
|
+
function classifyTokenFailure(error) {
|
|
2622
|
+
const code3 = error?.code;
|
|
2623
|
+
if (code3 === "SP500") return "unavailable";
|
|
2624
|
+
if (error?.type === "NotFound") {
|
|
2625
|
+
return "unavailable";
|
|
2626
|
+
}
|
|
2627
|
+
return "unavailable-temporarily";
|
|
2628
|
+
}
|
|
2629
|
+
function useDictation({
|
|
2630
|
+
region,
|
|
2631
|
+
locale,
|
|
2632
|
+
getToken,
|
|
2633
|
+
onPhrase,
|
|
2634
|
+
onInterim,
|
|
2635
|
+
maxDurationMs = 18e4,
|
|
2636
|
+
idleTimeoutMs = 2e4
|
|
2637
|
+
}) {
|
|
2638
|
+
const [state, setState] = useState("idle");
|
|
2639
|
+
const [error, setError] = useState(null);
|
|
2640
|
+
const [deviceLabel, setDeviceLabel] = useState("");
|
|
2641
|
+
const [silent, setSilent] = useState(false);
|
|
2642
|
+
const sessionRef = useRef(null);
|
|
2643
|
+
const generationRef = useRef(0);
|
|
2644
|
+
const onPhraseRef = useRef(onPhrase);
|
|
2645
|
+
const onInterimRef = useRef(onInterim);
|
|
2646
|
+
useEffect(() => {
|
|
2647
|
+
onPhraseRef.current = onPhrase;
|
|
2648
|
+
onInterimRef.current = onInterim;
|
|
2649
|
+
});
|
|
2650
|
+
const clearInterim = useCallback(() => onInterimRef.current?.(""), []);
|
|
2651
|
+
const supported = isSupported();
|
|
2652
|
+
const available = supported && !!region && !!getToken;
|
|
2653
|
+
useEffect(() => {
|
|
2654
|
+
if (!available) return;
|
|
2655
|
+
const nav = navigator;
|
|
2656
|
+
if (nav.connection?.saveData) return;
|
|
2657
|
+
const warm = () => void kickOff(() => import('microsoft-cognitiveservices-speech-sdk'));
|
|
2658
|
+
if (typeof window.requestIdleCallback === "function") {
|
|
2659
|
+
const id2 = window.requestIdleCallback(warm, { timeout: 2e3 });
|
|
2660
|
+
return () => window.cancelIdleCallback(id2);
|
|
2661
|
+
}
|
|
2662
|
+
const id = window.setTimeout(warm, 1500);
|
|
2663
|
+
return () => window.clearTimeout(id);
|
|
2664
|
+
}, [available]);
|
|
2665
|
+
const stop = useCallback(() => {
|
|
2666
|
+
generationRef.current += 1;
|
|
2667
|
+
const session = sessionRef.current;
|
|
2668
|
+
sessionRef.current = null;
|
|
2669
|
+
clearInterim();
|
|
2670
|
+
setState("idle");
|
|
2671
|
+
setSilent(false);
|
|
2672
|
+
setDeviceLabel("");
|
|
2673
|
+
if (!session) return;
|
|
2674
|
+
window.clearTimeout(session.timers.idle);
|
|
2675
|
+
window.clearTimeout(session.timers.max);
|
|
2676
|
+
session.stopLevelMonitor();
|
|
2677
|
+
session.stream.getTracks().forEach((t) => t.stop());
|
|
2678
|
+
session.recognizer.stopContinuousRecognitionAsync(
|
|
2679
|
+
() => session.recognizer.close(),
|
|
2680
|
+
() => session.recognizer.close()
|
|
2681
|
+
);
|
|
2682
|
+
}, [clearInterim]);
|
|
2683
|
+
const start = useCallback(async () => {
|
|
2684
|
+
if (!available || sessionRef.current) return;
|
|
2685
|
+
const generation = ++generationRef.current;
|
|
2686
|
+
const superseded = () => generationRef.current !== generation;
|
|
2687
|
+
setError(null);
|
|
2688
|
+
setSilent(false);
|
|
2689
|
+
setState("starting");
|
|
2690
|
+
const streamPromise = kickOff(
|
|
2691
|
+
() => navigator.mediaDevices.getUserMedia({
|
|
2692
|
+
audio: {
|
|
2693
|
+
echoCancellation: true,
|
|
2694
|
+
noiseSuppression: true,
|
|
2695
|
+
autoGainControl: true
|
|
2696
|
+
}
|
|
2697
|
+
})
|
|
2698
|
+
);
|
|
2699
|
+
const tokenPromise = kickOff(getToken);
|
|
2700
|
+
const sdkPromise = kickOff(
|
|
2701
|
+
() => import('microsoft-cognitiveservices-speech-sdk')
|
|
2702
|
+
);
|
|
2703
|
+
let stream;
|
|
2704
|
+
try {
|
|
2705
|
+
stream = await streamPromise;
|
|
2706
|
+
} catch (e) {
|
|
2707
|
+
if (superseded()) return;
|
|
2708
|
+
const name = e?.name;
|
|
2709
|
+
setError(
|
|
2710
|
+
name === "NotAllowedError" || name === "SecurityError" ? "permission-denied" : name === "NotFoundError" || name === "OverconstrainedError" ? "no-microphone" : "unknown"
|
|
2711
|
+
);
|
|
2712
|
+
setState("idle");
|
|
2713
|
+
return;
|
|
2714
|
+
}
|
|
2715
|
+
if (superseded()) {
|
|
2716
|
+
stream.getTracks().forEach((t) => t.stop());
|
|
2717
|
+
return;
|
|
2718
|
+
}
|
|
2719
|
+
let token;
|
|
2720
|
+
try {
|
|
2721
|
+
token = await tokenPromise;
|
|
2722
|
+
if (Date.parse(token.expiresOn) - Date.now() < STALE_TOKEN_FLOOR_MS) {
|
|
2723
|
+
token = await getToken();
|
|
2724
|
+
}
|
|
2725
|
+
} catch (e) {
|
|
2726
|
+
stream.getTracks().forEach((t) => t.stop());
|
|
2727
|
+
if (superseded()) return;
|
|
2728
|
+
setError(classifyTokenFailure(e));
|
|
2729
|
+
setState("idle");
|
|
2730
|
+
return;
|
|
2731
|
+
}
|
|
2732
|
+
if (superseded()) {
|
|
2733
|
+
stream.getTracks().forEach((t) => t.stop());
|
|
2734
|
+
return;
|
|
2735
|
+
}
|
|
2736
|
+
let recognizer = null;
|
|
2737
|
+
let published = null;
|
|
2738
|
+
try {
|
|
2739
|
+
const sdk = await sdkPromise;
|
|
2740
|
+
if (superseded()) {
|
|
2741
|
+
stream.getTracks().forEach((t) => t.stop());
|
|
2742
|
+
return;
|
|
2743
|
+
}
|
|
2744
|
+
const speechConfig = sdk.SpeechConfig.fromAuthorizationToken(
|
|
2745
|
+
token.token,
|
|
2746
|
+
region
|
|
2747
|
+
);
|
|
2748
|
+
speechConfig.speechRecognitionLanguage = locale;
|
|
2749
|
+
speechConfig.setProfanity(sdk.ProfanityOption.Raw);
|
|
2750
|
+
speechConfig.setProperty(
|
|
2751
|
+
sdk.PropertyId.Speech_SegmentationSilenceTimeoutMs,
|
|
2752
|
+
"700"
|
|
2753
|
+
);
|
|
2754
|
+
recognizer = new sdk.SpeechRecognizer(
|
|
2755
|
+
speechConfig,
|
|
2756
|
+
sdk.AudioConfig.fromStreamInput(stream)
|
|
2757
|
+
);
|
|
2758
|
+
const untilExpiry = Date.parse(token.expiresOn) - Date.now() - TOKEN_EXPIRY_MARGIN_MS;
|
|
2759
|
+
const sessionCapMs = Number.isFinite(untilExpiry) && untilExpiry > 0 ? Math.min(maxDurationMs, untilExpiry) : maxDurationMs;
|
|
2760
|
+
const session = {
|
|
2761
|
+
recognizer,
|
|
2762
|
+
stream,
|
|
2763
|
+
timers: {},
|
|
2764
|
+
stopLevelMonitor: noLevelMonitor
|
|
2765
|
+
};
|
|
2766
|
+
const armIdle = () => {
|
|
2767
|
+
window.clearTimeout(session.timers.idle);
|
|
2768
|
+
session.timers.idle = window.setTimeout(stop, idleTimeoutMs);
|
|
2769
|
+
};
|
|
2770
|
+
const isLive = () => sessionRef.current === session;
|
|
2771
|
+
sessionRef.current = session;
|
|
2772
|
+
published = session;
|
|
2773
|
+
const goLive = () => {
|
|
2774
|
+
if (!isLive()) return;
|
|
2775
|
+
session.timers.max ??= window.setTimeout(stop, sessionCapMs);
|
|
2776
|
+
if (session.stopLevelMonitor === noLevelMonitor) {
|
|
2777
|
+
try {
|
|
2778
|
+
setDeviceLabel(stream.getAudioTracks()[0]?.label ?? "");
|
|
2779
|
+
session.stopLevelMonitor = monitorInputLevel(
|
|
2780
|
+
stream,
|
|
2781
|
+
() => {
|
|
2782
|
+
if (isLive()) setSilent(true);
|
|
2783
|
+
},
|
|
2784
|
+
() => {
|
|
2785
|
+
if (isLive()) setSilent(false);
|
|
2786
|
+
}
|
|
2787
|
+
);
|
|
2788
|
+
} catch {
|
|
2789
|
+
session.stopLevelMonitor = noLevelMonitor;
|
|
2790
|
+
}
|
|
2791
|
+
}
|
|
2792
|
+
armIdle();
|
|
2793
|
+
setState("listening");
|
|
2794
|
+
};
|
|
2795
|
+
recognizer.sessionStarted = () => goLive();
|
|
2796
|
+
recognizer.recognizing = (_sender, event) => {
|
|
2797
|
+
if (!isLive()) return;
|
|
2798
|
+
onInterimRef.current?.(event.result.text ?? "");
|
|
2799
|
+
armIdle();
|
|
2800
|
+
};
|
|
2801
|
+
recognizer.recognized = (_sender, event) => {
|
|
2802
|
+
if (!isLive()) return;
|
|
2803
|
+
const text6 = event.result.text?.trim();
|
|
2804
|
+
clearInterim();
|
|
2805
|
+
if (event.result.reason === sdk.ResultReason.RecognizedSpeech && text6) {
|
|
2806
|
+
onPhraseRef.current(text6);
|
|
2807
|
+
}
|
|
2808
|
+
armIdle();
|
|
2809
|
+
};
|
|
2810
|
+
recognizer.canceled = (_sender, event) => {
|
|
2811
|
+
if (!isLive()) return;
|
|
2812
|
+
if (event.reason === sdk.CancellationReason.Error) {
|
|
2813
|
+
console.warn("[dictation] cancelled, errorCode:", event.errorCode);
|
|
2814
|
+
setError(
|
|
2815
|
+
event.errorCode === sdk.CancellationErrorCode.ConnectionFailure ? "network" : event.errorCode === sdk.CancellationErrorCode.AuthenticationFailure ? "unavailable-temporarily" : "unknown"
|
|
2816
|
+
);
|
|
2817
|
+
}
|
|
2818
|
+
stop();
|
|
2819
|
+
};
|
|
2820
|
+
recognizer.sessionStopped = () => {
|
|
2821
|
+
if (isLive()) stop();
|
|
2822
|
+
};
|
|
2823
|
+
try {
|
|
2824
|
+
const connection = sdk.Connection.fromRecognizer(recognizer);
|
|
2825
|
+
connection.connected = () => goLive();
|
|
2826
|
+
connection.openConnection();
|
|
2827
|
+
} catch {
|
|
2828
|
+
}
|
|
2829
|
+
await new Promise(
|
|
2830
|
+
(resolve, reject) => session.recognizer.startContinuousRecognitionAsync(resolve, reject)
|
|
2831
|
+
);
|
|
2832
|
+
goLive();
|
|
2833
|
+
} catch (e) {
|
|
2834
|
+
stream.getTracks().forEach((t) => t.stop());
|
|
2835
|
+
try {
|
|
2836
|
+
recognizer?.close();
|
|
2837
|
+
} catch {
|
|
2838
|
+
}
|
|
2839
|
+
if (published) {
|
|
2840
|
+
window.clearTimeout(published.timers.idle);
|
|
2841
|
+
window.clearTimeout(published.timers.max);
|
|
2842
|
+
published.stopLevelMonitor();
|
|
2843
|
+
if (sessionRef.current === published) sessionRef.current = null;
|
|
2844
|
+
}
|
|
2845
|
+
if (superseded()) return;
|
|
2846
|
+
console.warn("[dictation] failed to start:", e);
|
|
2847
|
+
setError((prev) => prev ?? "unavailable-temporarily");
|
|
2848
|
+
setState("idle");
|
|
2849
|
+
}
|
|
2850
|
+
}, [
|
|
2851
|
+
available,
|
|
2852
|
+
clearInterim,
|
|
2853
|
+
getToken,
|
|
2854
|
+
idleTimeoutMs,
|
|
2855
|
+
locale,
|
|
2856
|
+
maxDurationMs,
|
|
2857
|
+
region,
|
|
2858
|
+
stop
|
|
2859
|
+
]);
|
|
2860
|
+
useEffect(() => stop, [stop]);
|
|
2861
|
+
const toggle = useCallback(() => {
|
|
2862
|
+
if (state === "idle") void start();
|
|
2863
|
+
else stop();
|
|
2864
|
+
}, [start, state, stop]);
|
|
2865
|
+
return {
|
|
2866
|
+
supported,
|
|
2867
|
+
available,
|
|
2868
|
+
state,
|
|
2869
|
+
error,
|
|
2870
|
+
/** Name of the microphone in use, once a session is live. May be ''. */
|
|
2871
|
+
deviceLabel,
|
|
2872
|
+
/** Listening, but the microphone has produced no sound for a while. */
|
|
2873
|
+
silent,
|
|
2874
|
+
start,
|
|
2875
|
+
stop,
|
|
2876
|
+
toggle
|
|
2877
|
+
};
|
|
2878
|
+
}
|
|
2440
2879
|
function useFlowRunVariables(flowRunId) {
|
|
2441
2880
|
const service = useChatService();
|
|
2442
2881
|
return useQuery({
|
|
@@ -3742,6 +4181,58 @@ var ChatVariablesForm = forwardRef(({ workspace, threadId, setVariables }, ref)
|
|
|
3742
4181
|
) }) })
|
|
3743
4182
|
] });
|
|
3744
4183
|
});
|
|
4184
|
+
var dictationErrorMessages = {
|
|
4185
|
+
"permission-denied": "Microphone access is blocked \u2014 allow it in your browser settings and try again",
|
|
4186
|
+
"no-microphone": "No microphone was found",
|
|
4187
|
+
network: "Could not reach the speech service \u2014 try again",
|
|
4188
|
+
"unavailable-temporarily": "Dictation is starting up \u2014 try again in a moment",
|
|
4189
|
+
unavailable: "Dictation is not available on this workspace",
|
|
4190
|
+
unknown: "Dictation stopped unexpectedly \u2014 try again"
|
|
4191
|
+
};
|
|
4192
|
+
function DictationButton({
|
|
4193
|
+
state,
|
|
4194
|
+
error,
|
|
4195
|
+
disabled,
|
|
4196
|
+
onToggle,
|
|
4197
|
+
deviceLabel,
|
|
4198
|
+
size = "sm"
|
|
4199
|
+
}) {
|
|
4200
|
+
const listening = state === "listening";
|
|
4201
|
+
const starting = state === "starting";
|
|
4202
|
+
const label = listening ? "Stop dictation" : "Dictate a message";
|
|
4203
|
+
const hint = listening && deviceLabel ? `${label} \u2014 ${deviceLabel}` : label;
|
|
4204
|
+
const dims = size === "sm" ? "h-8 w-8" : "h-9 w-9";
|
|
4205
|
+
const icon = size === "sm" ? "h-4 w-4" : "h-5 w-5";
|
|
4206
|
+
return /* @__PURE__ */ jsxs(
|
|
4207
|
+
IconButton2,
|
|
4208
|
+
{
|
|
4209
|
+
type: "button",
|
|
4210
|
+
onClick: onToggle,
|
|
4211
|
+
disabled,
|
|
4212
|
+
"aria-pressed": listening,
|
|
4213
|
+
"aria-label": label,
|
|
4214
|
+
title: error ? dictationErrorMessages[error] : hint,
|
|
4215
|
+
className: `relative ${dims} rounded-full transition-colors ${listening ? "bg-destructive text-destructive-foreground hover:bg-destructive/90" : "text-muted-foreground hover:bg-secondary"}`,
|
|
4216
|
+
children: [
|
|
4217
|
+
starting && /* @__PURE__ */ jsx(
|
|
4218
|
+
"span",
|
|
4219
|
+
{
|
|
4220
|
+
"aria-hidden": "true",
|
|
4221
|
+
className: "absolute inset-0 animate-spin rounded-full border-2 border-transparent border-t-current opacity-70"
|
|
4222
|
+
}
|
|
4223
|
+
),
|
|
4224
|
+
listening && /* @__PURE__ */ jsx(
|
|
4225
|
+
"span",
|
|
4226
|
+
{
|
|
4227
|
+
"aria-hidden": "true",
|
|
4228
|
+
className: "absolute inset-0 animate-ping rounded-full bg-destructive/30"
|
|
4229
|
+
}
|
|
4230
|
+
),
|
|
4231
|
+
listening ? /* @__PURE__ */ jsx(Square, { className: `relative ${icon} fill-current` }) : /* @__PURE__ */ jsx(Mic, { className: `relative ${icon}` })
|
|
4232
|
+
]
|
|
4233
|
+
}
|
|
4234
|
+
);
|
|
4235
|
+
}
|
|
3745
4236
|
|
|
3746
4237
|
// src/domains/workspaces/queryKeys.ts
|
|
3747
4238
|
var workspaceKeys = {
|
|
@@ -3974,6 +4465,40 @@ function MessageComposer(_props = {}) {
|
|
|
3974
4465
|
cancelReset();
|
|
3975
4466
|
}
|
|
3976
4467
|
}, [isSending, cancelAccepted, cancelErrored, cancelReset]);
|
|
4468
|
+
const attachEditor = useCallback(
|
|
4469
|
+
(handle2) => {
|
|
4470
|
+
if (handle2) editorRef.current = handle2;
|
|
4471
|
+
},
|
|
4472
|
+
// Both flags matter: the portal mounts on `isMobile && isFullscreen`, and
|
|
4473
|
+
// nothing resets isFullscreen when the breakpoint flips, so crossing it
|
|
4474
|
+
// while fullscreen would unmount the portal editor without re-attaching the
|
|
4475
|
+
// inline one — leaving a destroyed handle and silently swallowing phrases.
|
|
4476
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
4477
|
+
[isFullscreen, isMobile]
|
|
4478
|
+
);
|
|
4479
|
+
const { data: speechConfig, isFetching: speechConfigLoading } = useSpeechConfig();
|
|
4480
|
+
const getSpeechToken = useMemo(
|
|
4481
|
+
() => chatService.getSpeechToken?.bind(chatService),
|
|
4482
|
+
[chatService]
|
|
4483
|
+
);
|
|
4484
|
+
const dictation = useDictation({
|
|
4485
|
+
region: speechConfig?.enabled ? speechConfig.region : null,
|
|
4486
|
+
locale: speechConfig?.defaultLocale ?? "en-US",
|
|
4487
|
+
getToken: getSpeechToken,
|
|
4488
|
+
// Final phrases become real text; provisional ones render as greyed ghost
|
|
4489
|
+
// text at the caret and never enter the document.
|
|
4490
|
+
onPhrase: (text6) => editorRef.current?.insertText(`${text6} `),
|
|
4491
|
+
onInterim: (text6) => editorRef.current?.setDictationGhost(text6)
|
|
4492
|
+
});
|
|
4493
|
+
const { stop: stopDictation } = dictation;
|
|
4494
|
+
const dictationUnavailableReason = speechConfigLoading ? "Checking whether dictation is available" : dictation.supported ? "Dictation is not enabled for this workspace" : "Dictation is not available in this browser";
|
|
4495
|
+
const handleDictationToggle = () => {
|
|
4496
|
+
if (dictation.state === "idle") editorRef.current?.focus();
|
|
4497
|
+
dictation.toggle();
|
|
4498
|
+
};
|
|
4499
|
+
useEffect(() => {
|
|
4500
|
+
if (disabled) stopDictation();
|
|
4501
|
+
}, [disabled, stopDictation]);
|
|
3977
4502
|
useEffect(() => {
|
|
3978
4503
|
if (typeof window === "undefined") return;
|
|
3979
4504
|
window.__ssDownloadFile = (id) => getFileBlobUrl(id);
|
|
@@ -4093,6 +4618,7 @@ function MessageComposer(_props = {}) {
|
|
|
4093
4618
|
const latestText = editorRef.current?.getMarkdown() ?? newMessage;
|
|
4094
4619
|
const sent = handleSendMessage(latestText, uploadedAttachments);
|
|
4095
4620
|
if (!sent) return;
|
|
4621
|
+
stopDictation();
|
|
4096
4622
|
handleClearAttachments();
|
|
4097
4623
|
setEditorKey((k) => k + 1);
|
|
4098
4624
|
};
|
|
@@ -4199,7 +4725,7 @@ function MessageComposer(_props = {}) {
|
|
|
4199
4725
|
/* @__PURE__ */ jsx("div", { className: "max-h-[400px] w-full overflow-y-auto", children: /* @__PURE__ */ jsx(
|
|
4200
4726
|
MarkdownEditor,
|
|
4201
4727
|
{
|
|
4202
|
-
ref:
|
|
4728
|
+
ref: attachEditor,
|
|
4203
4729
|
value: newMessage,
|
|
4204
4730
|
onChange: (md) => setNewMessage(md),
|
|
4205
4731
|
onKeyDown: handleComposerKeyDown,
|
|
@@ -4216,10 +4742,18 @@ function MessageComposer(_props = {}) {
|
|
|
4216
4742
|
},
|
|
4217
4743
|
`composer-md-${editorKey}`
|
|
4218
4744
|
) }),
|
|
4745
|
+
/* @__PURE__ */ jsx("div", { role: "status", className: "sr-only", children: dictation.state === "listening" ? dictation.silent ? `Listening, but not hearing anything from ${dictation.deviceLabel || "your microphone"}` : "Listening" : dictation.state === "starting" ? "Starting dictation" : "" }),
|
|
4746
|
+
dictation.state === "listening" && dictation.silent && /* @__PURE__ */ jsxs("div", { className: "px-5 pb-1 text-xs text-muted-foreground", children: [
|
|
4747
|
+
"Not hearing anything from",
|
|
4748
|
+
" ",
|
|
4749
|
+
dictation.deviceLabel || "your microphone",
|
|
4750
|
+
"."
|
|
4751
|
+
] }),
|
|
4752
|
+
dictation.error && /* @__PURE__ */ jsx("div", { role: "alert", className: "px-5 pb-1 text-xs text-destructive", children: dictationErrorMessages[dictation.error] }),
|
|
4219
4753
|
/* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between gap-2 px-3 pb-3", children: [
|
|
4220
4754
|
/* @__PURE__ */ jsxs("div", { className: "ss-composer__actions flex min-w-0 flex-1 items-center gap-1", children: [
|
|
4221
4755
|
supportsFiles && /* @__PURE__ */ jsx(
|
|
4222
|
-
|
|
4756
|
+
IconButton2,
|
|
4223
4757
|
{
|
|
4224
4758
|
type: "button",
|
|
4225
4759
|
onClick: handlePickFilesClick,
|
|
@@ -4240,19 +4774,39 @@ function MessageComposer(_props = {}) {
|
|
|
4240
4774
|
)
|
|
4241
4775
|
] }),
|
|
4242
4776
|
/* @__PURE__ */ jsxs("div", { className: "flex shrink-0 items-center gap-1.5", children: [
|
|
4243
|
-
/* @__PURE__ */ jsx(
|
|
4244
|
-
|
|
4777
|
+
dictation.available ? /* @__PURE__ */ jsx(
|
|
4778
|
+
DictationButton,
|
|
4245
4779
|
{
|
|
4246
|
-
|
|
4247
|
-
|
|
4248
|
-
|
|
4249
|
-
|
|
4250
|
-
|
|
4251
|
-
children: /* @__PURE__ */ jsx(Mic, { className: "h-4 w-4" })
|
|
4780
|
+
state: dictation.state,
|
|
4781
|
+
error: dictation.error,
|
|
4782
|
+
disabled,
|
|
4783
|
+
onToggle: handleDictationToggle,
|
|
4784
|
+
deviceLabel: dictation.deviceLabel
|
|
4252
4785
|
}
|
|
4786
|
+
) : (
|
|
4787
|
+
// A disabled button receives no pointer events, so its own
|
|
4788
|
+
// `title` never renders a tooltip. The wrapper is what the user
|
|
4789
|
+
// actually hovers.
|
|
4790
|
+
/* @__PURE__ */ jsx(
|
|
4791
|
+
"span",
|
|
4792
|
+
{
|
|
4793
|
+
title: dictationUnavailableReason,
|
|
4794
|
+
className: "inline-flex cursor-not-allowed",
|
|
4795
|
+
children: /* @__PURE__ */ jsx(
|
|
4796
|
+
IconButton2,
|
|
4797
|
+
{
|
|
4798
|
+
type: "button",
|
|
4799
|
+
disabled: true,
|
|
4800
|
+
"aria-label": `Dictate a message. ${dictationUnavailableReason}`,
|
|
4801
|
+
className: "h-8 w-8 rounded-full text-muted-foreground",
|
|
4802
|
+
children: /* @__PURE__ */ jsx(Mic, { className: "h-4 w-4" })
|
|
4803
|
+
}
|
|
4804
|
+
)
|
|
4805
|
+
}
|
|
4806
|
+
)
|
|
4253
4807
|
),
|
|
4254
4808
|
canStop ? /* @__PURE__ */ jsx(
|
|
4255
|
-
|
|
4809
|
+
IconButton2,
|
|
4256
4810
|
{
|
|
4257
4811
|
onClick: handleStopRun,
|
|
4258
4812
|
className: "chat-send h-8 w-8 rounded-full",
|
|
@@ -4266,7 +4820,7 @@ function MessageComposer(_props = {}) {
|
|
|
4266
4820
|
)
|
|
4267
4821
|
}
|
|
4268
4822
|
) : /* @__PURE__ */ jsx(
|
|
4269
|
-
|
|
4823
|
+
IconButton2,
|
|
4270
4824
|
{
|
|
4271
4825
|
onClick: handleSendMessageAndClear,
|
|
4272
4826
|
className: `chat-send h-8 w-8 rounded-full ${sendDisabled ? "cursor-not-allowed" : ""}`,
|
|
@@ -4295,7 +4849,7 @@ function MessageComposer(_props = {}) {
|
|
|
4295
4849
|
},
|
|
4296
4850
|
children: /* @__PURE__ */ jsxs("div", { className: "relative h-full w-full bg-background border shadow-lg", children: [
|
|
4297
4851
|
/* @__PURE__ */ jsx(
|
|
4298
|
-
|
|
4852
|
+
IconButton2,
|
|
4299
4853
|
{
|
|
4300
4854
|
type: "button",
|
|
4301
4855
|
size: "small",
|
|
@@ -4313,7 +4867,7 @@ function MessageComposer(_props = {}) {
|
|
|
4313
4867
|
/* @__PURE__ */ jsx("div", { className: "flex-1 p-4", children: /* @__PURE__ */ jsx(
|
|
4314
4868
|
MarkdownEditor,
|
|
4315
4869
|
{
|
|
4316
|
-
ref:
|
|
4870
|
+
ref: attachEditor,
|
|
4317
4871
|
value: newMessage,
|
|
4318
4872
|
onChange: (md) => setNewMessage(md),
|
|
4319
4873
|
onKeyDown: handleComposerKeyDown,
|
|
@@ -4328,10 +4882,18 @@ function MessageComposer(_props = {}) {
|
|
|
4328
4882
|
},
|
|
4329
4883
|
`composer-md-${editorKey}`
|
|
4330
4884
|
) }),
|
|
4885
|
+
dictation.error && /* @__PURE__ */ jsx(
|
|
4886
|
+
"div",
|
|
4887
|
+
{
|
|
4888
|
+
"aria-hidden": "true",
|
|
4889
|
+
className: "px-4 pb-1 text-xs text-destructive",
|
|
4890
|
+
children: dictationErrorMessages[dictation.error]
|
|
4891
|
+
}
|
|
4892
|
+
),
|
|
4331
4893
|
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 px-3 py-2 border-t bg-background", children: [
|
|
4332
4894
|
/* @__PURE__ */ jsx("div", { className: "flex-1" }),
|
|
4333
4895
|
supportsFiles && /* @__PURE__ */ jsx(
|
|
4334
|
-
|
|
4896
|
+
IconButton2,
|
|
4335
4897
|
{
|
|
4336
4898
|
type: "button",
|
|
4337
4899
|
onClick: handlePickFilesClick,
|
|
@@ -4346,8 +4908,19 @@ function MessageComposer(_props = {}) {
|
|
|
4346
4908
|
)
|
|
4347
4909
|
}
|
|
4348
4910
|
),
|
|
4911
|
+
dictation.available && /* @__PURE__ */ jsx(
|
|
4912
|
+
DictationButton,
|
|
4913
|
+
{
|
|
4914
|
+
size: "md",
|
|
4915
|
+
state: dictation.state,
|
|
4916
|
+
error: dictation.error,
|
|
4917
|
+
disabled,
|
|
4918
|
+
onToggle: handleDictationToggle,
|
|
4919
|
+
deviceLabel: dictation.deviceLabel
|
|
4920
|
+
}
|
|
4921
|
+
),
|
|
4349
4922
|
canStop ? /* @__PURE__ */ jsx(
|
|
4350
|
-
|
|
4923
|
+
IconButton2,
|
|
4351
4924
|
{
|
|
4352
4925
|
onClick: handleStopRun,
|
|
4353
4926
|
className: "chat-send h-9 w-9 rounded-full",
|
|
@@ -4361,7 +4934,7 @@ function MessageComposer(_props = {}) {
|
|
|
4361
4934
|
)
|
|
4362
4935
|
}
|
|
4363
4936
|
) : /* @__PURE__ */ jsx(
|
|
4364
|
-
|
|
4937
|
+
IconButton2,
|
|
4365
4938
|
{
|
|
4366
4939
|
onClick: handleSendMessageAndClear,
|
|
4367
4940
|
className: `chat-send h-9 w-9 rounded-full ${sendDisabled ? "cursor-not-allowed" : ""}`,
|
|
@@ -20696,6 +21269,6 @@ function getUserPhotoUrl(userId) {
|
|
|
20696
21269
|
return `${base2}/users/${userId}/photo`;
|
|
20697
21270
|
}
|
|
20698
21271
|
|
|
20699
|
-
export { ChatProvider, ChatVariablesForm, DRAFT_THREAD_PREFIX, DateFromApi, MarkdownEditor, MessageComposer, MessageList, MessageListSkeleton, MessageMarkdown, MessageValueType, NEW_THREAD_ID, THREAD_LIST_PAGE_SIZE, applyDeltaToMessage, applyThreadToCache, createDraftThreadId, createThreadId, downloadFileBlobOptions, filesKeys, flowRunsKeys, getMessageErrorText, getModelIcon, getRetryStatusText, getThreadPlaceholderFromListCache, getUserPhotoUrl, invalidateWorkspaceThreadLists, isDraftThreadId, mapFileInfoDtoToModel, mapMentionUserDtoToModel, mapMessageDtoToModel, mapMessageErrorDtoToModel, mapMessageValueDtoToModel, mapMessagesDtoToModels, mapSignalRThreadSummaryToModel, mapThreadDtoToModel, mapThreadsResponseDtoToModel, mapWorkspaceDtoToModel, mapWorkspacesDtoToModels, markDraftThreadId, messagesKeys, messagesListOptions, messagesMutationsKeys, modelsKeys, parseDateTime, parseDateTimeHuman, parseRetryStatus, randomUUID, setThreadOptimisticRunning, setThreadRunningInLists, taggableUsersOptions, threadDetailOptions, threadsKeys, unmarkDraftThreadId, useAddInputToMessage, useChatContext, useChatIdentity, useChatService, useDownloadFileBlobQuery, useFileMutations, useFlowRunVariables, useIsDraftThreadId, useMessages, useModels, useSendMessage, useTaggableWorkspaceUsers, useThread, useThreadIsRunning, useUpdateFlowRunVariable, useWorkspace, utcDate, workspaceDetailOptions, workspaceKeys };
|
|
21272
|
+
export { ChatProvider, ChatVariablesForm, DRAFT_THREAD_PREFIX, DateFromApi, DictationButton, MarkdownEditor, MessageComposer, MessageList, MessageListSkeleton, MessageMarkdown, MessageValueType, NEW_THREAD_ID, THREAD_LIST_PAGE_SIZE, applyDeltaToMessage, applyThreadToCache, createDraftThreadId, createThreadId, downloadFileBlobOptions, filesKeys, flowRunsKeys, getMessageErrorText, getModelIcon, getRetryStatusText, getThreadPlaceholderFromListCache, getUserPhotoUrl, invalidateWorkspaceThreadLists, isDraftThreadId, mapFileInfoDtoToModel, mapMentionUserDtoToModel, mapMessageDtoToModel, mapMessageErrorDtoToModel, mapMessageValueDtoToModel, mapMessagesDtoToModels, mapSignalRThreadSummaryToModel, mapThreadDtoToModel, mapThreadsResponseDtoToModel, mapWorkspaceDtoToModel, mapWorkspacesDtoToModels, markDraftThreadId, messagesKeys, messagesListOptions, messagesMutationsKeys, modelsKeys, parseDateTime, parseDateTimeHuman, parseRetryStatus, randomUUID, setThreadOptimisticRunning, setThreadRunningInLists, speechKeys, taggableUsersOptions, threadDetailOptions, threadsKeys, unmarkDraftThreadId, useAddInputToMessage, useChatContext, useChatIdentity, useChatService, useDictation, useDownloadFileBlobQuery, useFileMutations, useFlowRunVariables, useIsDraftThreadId, useMessages, useModels, useSendMessage, useSpeechConfig, useTaggableWorkspaceUsers, useThread, useThreadIsRunning, useUpdateFlowRunVariable, useWorkspace, utcDate, workspaceDetailOptions, workspaceKeys };
|
|
20700
21273
|
//# sourceMappingURL=index.js.map
|
|
20701
21274
|
//# sourceMappingURL=index.js.map
|