@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.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import MuiButton from '@mui/material/Button';
2
- import IconButton from '@mui/material/IconButton';
3
- import { Loader2, ChevronDown, Check, Cpu, SlidersHorizontal, X, Globe, Zap, Paperclip, Mic, Square, Send, Minimize2, AlertTriangle, FileImage, FileVideo, FileAudio, FileArchive, FileCode, FileSpreadsheet, Presentation, FileText, ChevronUp, ExternalLink, Download, ShieldAlert, Copy } from 'lucide-react';
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 { Decoration, DecorationSet } from '@milkdown/prose/view';
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: editorRef,
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
- IconButton,
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
- IconButton,
4777
+ dictation.available ? /* @__PURE__ */ jsx(
4778
+ DictationButton,
4245
4779
  {
4246
- type: "button",
4247
- disabled: true,
4248
- "aria-label": "Dictate a message",
4249
- title: "Dictation is not available yet",
4250
- className: "h-8 w-8 cursor-not-allowed rounded-full text-muted-foreground",
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
- IconButton,
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
- IconButton,
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
- IconButton,
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: editorRef,
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
- IconButton,
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
- IconButton,
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
- IconButton,
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