@smartspace/chat-ui 1.14.6-main.7d918a6 → 1.14.6-main.84a0dbd

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,11 +1,11 @@
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, ChevronRight, ChevronUp, ExternalLink, Download, ShieldAlert, Copy } from 'lucide-react';
4
4
  import * as React9 from 'react';
5
- import React9__default, { createContext, forwardRef, useImperativeHandle, useRef, useState, useEffect, useMemo, useCallback, useContext, useSyncExternalStore } from 'react';
5
+ import React9__default, { createContext, forwardRef, useImperativeHandle, useRef, useState, useEffect, useMemo, useCallback, useContext, useSyncExternalStore, Fragment as Fragment$1 } from 'react';
6
6
  import { createPortal } from 'react-dom';
7
7
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
8
- import { useQuery, queryOptions, useQueryClient, useMutation, skipToken } from '@tanstack/react-query';
8
+ import { useQuery, queryOptions, useQueryClient, useMutation } from '@tanstack/react-query';
9
9
  import { toast } from 'sonner';
10
10
  import { ChatZod } from '@smartspace/api-client';
11
11
  import { z } from 'zod';
@@ -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';
@@ -600,9 +600,14 @@ var useThread = ({
600
600
  };
601
601
  var useThreadIsRunning = (workspaceId, threadId) => {
602
602
  const queryClient = useQueryClient();
603
+ const service = useChatService();
603
604
  const { data: detailThread } = useQuery({
604
- queryKey: threadsKeys.detail(workspaceId ?? "", threadId ?? ""),
605
- queryFn: skipToken
605
+ ...threadDetailOptions({
606
+ service,
607
+ workspaceId: workspaceId ?? "",
608
+ threadId: threadId ?? ""
609
+ }),
610
+ enabled: false
606
611
  });
607
612
  const listThread = workspaceId && threadId ? getThreadPlaceholderFromListCache(queryClient, workspaceId, threadId) : void 0;
608
613
  const { data: optimistic } = useQuery({
@@ -837,6 +842,37 @@ function useMessages(threadId) {
837
842
  });
838
843
  }
839
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
+
840
876
  // ../../node_modules/.pnpm/@milkdown+exception@7.20.0/node_modules/@milkdown/exception/lib/index.js
841
877
  var ErrorCode = /* @__PURE__ */ (function(ErrorCode2) {
842
878
  ErrorCode2["docTypeError"] = "docTypeError";
@@ -970,6 +1006,75 @@ var autolink = $prose(() => {
970
1006
  }
971
1007
  });
972
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
+ }
973
1078
 
974
1079
  // src/shared/markdown/extensions/fileTag.ts
975
1080
  var fileTag = $node("fileTag", () => ({
@@ -1879,7 +1984,7 @@ function EditorInner({
1879
1984
  } catch {
1880
1985
  }
1881
1986
  });
1882
- }).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);
1883
1988
  },
1884
1989
  [isEditable]
1885
1990
  );
@@ -2089,6 +2194,25 @@ function EditorInner({
2089
2194
  return value ?? "";
2090
2195
  }
2091
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
+ },
2092
2216
  clear: () => {
2093
2217
  const view = viewRef.current;
2094
2218
  if (!view) return;
@@ -2432,6 +2556,326 @@ var MarkdownEditor = forwardRef((props, ref) => {
2432
2556
  return /* @__PURE__ */ jsx(MilkdownProvider, { children: /* @__PURE__ */ jsx(EditorInner, { ...props, editorHandleRef: ref }) });
2433
2557
  });
2434
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
+ }
2435
2879
  function useFlowRunVariables(flowRunId) {
2436
2880
  const service = useChatService();
2437
2881
  return useQuery({
@@ -3737,6 +4181,58 @@ var ChatVariablesForm = forwardRef(({ workspace, threadId, setVariables }, ref)
3737
4181
  ) }) })
3738
4182
  ] });
3739
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
+ }
3740
4236
 
3741
4237
  // src/domains/workspaces/queryKeys.ts
3742
4238
  var workspaceKeys = {
@@ -3969,6 +4465,40 @@ function MessageComposer(_props = {}) {
3969
4465
  cancelReset();
3970
4466
  }
3971
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]);
3972
4502
  useEffect(() => {
3973
4503
  if (typeof window === "undefined") return;
3974
4504
  window.__ssDownloadFile = (id) => getFileBlobUrl(id);
@@ -4088,6 +4618,7 @@ function MessageComposer(_props = {}) {
4088
4618
  const latestText = editorRef.current?.getMarkdown() ?? newMessage;
4089
4619
  const sent = handleSendMessage(latestText, uploadedAttachments);
4090
4620
  if (!sent) return;
4621
+ stopDictation();
4091
4622
  handleClearAttachments();
4092
4623
  setEditorKey((k) => k + 1);
4093
4624
  };
@@ -4194,7 +4725,7 @@ function MessageComposer(_props = {}) {
4194
4725
  /* @__PURE__ */ jsx("div", { className: "max-h-[400px] w-full overflow-y-auto", children: /* @__PURE__ */ jsx(
4195
4726
  MarkdownEditor,
4196
4727
  {
4197
- ref: editorRef,
4728
+ ref: attachEditor,
4198
4729
  value: newMessage,
4199
4730
  onChange: (md) => setNewMessage(md),
4200
4731
  onKeyDown: handleComposerKeyDown,
@@ -4211,10 +4742,18 @@ function MessageComposer(_props = {}) {
4211
4742
  },
4212
4743
  `composer-md-${editorKey}`
4213
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] }),
4214
4753
  /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between gap-2 px-3 pb-3", children: [
4215
4754
  /* @__PURE__ */ jsxs("div", { className: "ss-composer__actions flex min-w-0 flex-1 items-center gap-1", children: [
4216
4755
  supportsFiles && /* @__PURE__ */ jsx(
4217
- IconButton,
4756
+ IconButton2,
4218
4757
  {
4219
4758
  type: "button",
4220
4759
  onClick: handlePickFilesClick,
@@ -4235,19 +4774,39 @@ function MessageComposer(_props = {}) {
4235
4774
  )
4236
4775
  ] }),
4237
4776
  /* @__PURE__ */ jsxs("div", { className: "flex shrink-0 items-center gap-1.5", children: [
4238
- /* @__PURE__ */ jsx(
4239
- IconButton,
4777
+ dictation.available ? /* @__PURE__ */ jsx(
4778
+ DictationButton,
4240
4779
  {
4241
- type: "button",
4242
- disabled: true,
4243
- "aria-label": "Dictate a message",
4244
- title: "Dictation is not available yet",
4245
- className: "h-8 w-8 cursor-not-allowed rounded-full text-muted-foreground",
4246
- 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
4247
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
+ )
4248
4807
  ),
4249
4808
  canStop ? /* @__PURE__ */ jsx(
4250
- IconButton,
4809
+ IconButton2,
4251
4810
  {
4252
4811
  onClick: handleStopRun,
4253
4812
  className: "chat-send h-8 w-8 rounded-full",
@@ -4261,7 +4820,7 @@ function MessageComposer(_props = {}) {
4261
4820
  )
4262
4821
  }
4263
4822
  ) : /* @__PURE__ */ jsx(
4264
- IconButton,
4823
+ IconButton2,
4265
4824
  {
4266
4825
  onClick: handleSendMessageAndClear,
4267
4826
  className: `chat-send h-8 w-8 rounded-full ${sendDisabled ? "cursor-not-allowed" : ""}`,
@@ -4290,7 +4849,7 @@ function MessageComposer(_props = {}) {
4290
4849
  },
4291
4850
  children: /* @__PURE__ */ jsxs("div", { className: "relative h-full w-full bg-background border shadow-lg", children: [
4292
4851
  /* @__PURE__ */ jsx(
4293
- IconButton,
4852
+ IconButton2,
4294
4853
  {
4295
4854
  type: "button",
4296
4855
  size: "small",
@@ -4308,7 +4867,7 @@ function MessageComposer(_props = {}) {
4308
4867
  /* @__PURE__ */ jsx("div", { className: "flex-1 p-4", children: /* @__PURE__ */ jsx(
4309
4868
  MarkdownEditor,
4310
4869
  {
4311
- ref: editorRef,
4870
+ ref: attachEditor,
4312
4871
  value: newMessage,
4313
4872
  onChange: (md) => setNewMessage(md),
4314
4873
  onKeyDown: handleComposerKeyDown,
@@ -4323,10 +4882,18 @@ function MessageComposer(_props = {}) {
4323
4882
  },
4324
4883
  `composer-md-${editorKey}`
4325
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
+ ),
4326
4893
  /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 px-3 py-2 border-t bg-background", children: [
4327
4894
  /* @__PURE__ */ jsx("div", { className: "flex-1" }),
4328
4895
  supportsFiles && /* @__PURE__ */ jsx(
4329
- IconButton,
4896
+ IconButton2,
4330
4897
  {
4331
4898
  type: "button",
4332
4899
  onClick: handlePickFilesClick,
@@ -4341,8 +4908,19 @@ function MessageComposer(_props = {}) {
4341
4908
  )
4342
4909
  }
4343
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
+ ),
4344
4922
  canStop ? /* @__PURE__ */ jsx(
4345
- IconButton,
4923
+ IconButton2,
4346
4924
  {
4347
4925
  onClick: handleStopRun,
4348
4926
  className: "chat-send h-9 w-9 rounded-full",
@@ -4356,7 +4934,7 @@ function MessageComposer(_props = {}) {
4356
4934
  )
4357
4935
  }
4358
4936
  ) : /* @__PURE__ */ jsx(
4359
- IconButton,
4937
+ IconButton2,
4360
4938
  {
4361
4939
  onClick: handleSendMessageAndClear,
4362
4940
  className: `chat-send h-9 w-9 rounded-full ${sendDisabled ? "cursor-not-allowed" : ""}`,
@@ -19070,6 +19648,34 @@ var getMessageErrorText = (error) => {
19070
19648
  }
19071
19649
  return STATUS_CODE_TEXT[error.code] ?? GENERIC_ERROR_TEXT;
19072
19650
  };
19651
+ function extractProviderErrorDetail(raw2) {
19652
+ const start = raw2.indexOf("{");
19653
+ if (start === -1) return null;
19654
+ let parsed;
19655
+ try {
19656
+ parsed = JSON.parse(raw2.slice(start));
19657
+ } catch {
19658
+ return null;
19659
+ }
19660
+ if (typeof parsed !== "object" || parsed === null) return null;
19661
+ const record = parsed;
19662
+ const inner = typeof record.error === "object" && record.error !== null ? record.error : record;
19663
+ if (typeof inner.message !== "string") return null;
19664
+ return {
19665
+ message: inner.message,
19666
+ param: typeof inner.param === "string" ? inner.param : void 0
19667
+ };
19668
+ }
19669
+ var getMessageErrorDetail = (error) => {
19670
+ if (typeof error === "number" || !error.message) {
19671
+ return null;
19672
+ }
19673
+ const detail = extractProviderErrorDetail(error.message);
19674
+ if (!detail) {
19675
+ return error.message;
19676
+ }
19677
+ return detail.param ? `${detail.message} (param: ${detail.param})` : detail.message;
19678
+ };
19073
19679
  var FileInfoSchema = z.object({
19074
19680
  id: z.string(),
19075
19681
  name: z.string()
@@ -19961,6 +20567,25 @@ var MessageBubble = (props) => {
19961
20567
  /* @__PURE__ */ jsx("div", { className: "mt-1.5", children: metaRow })
19962
20568
  ] });
19963
20569
  };
20570
+ function MessageErrorDetails({ detail }) {
20571
+ const [expanded, setExpanded] = useState(false);
20572
+ return /* @__PURE__ */ jsxs("div", { className: "mt-1", children: [
20573
+ /* @__PURE__ */ jsxs(
20574
+ "button",
20575
+ {
20576
+ type: "button",
20577
+ onClick: () => setExpanded((prev) => !prev),
20578
+ "aria-expanded": expanded,
20579
+ className: "m-0 inline-flex w-auto cursor-pointer appearance-none items-center gap-1 border-0 bg-transparent p-0 text-[11px] font-medium leading-none text-muted-foreground hover:text-foreground",
20580
+ children: [
20581
+ expanded ? /* @__PURE__ */ jsx(ChevronDown, { className: "h-3 w-3 shrink-0" }) : /* @__PURE__ */ jsx(ChevronRight, { className: "h-3 w-3 shrink-0" }),
20582
+ expanded ? "Hide details" : "Show details"
20583
+ ]
20584
+ }
20585
+ ),
20586
+ expanded && /* @__PURE__ */ jsx("div", { className: "mt-1.5 whitespace-pre-wrap break-words rounded-md bg-secondary/60 p-2 font-mono text-[11px] leading-relaxed text-muted-foreground", children: detail })
20587
+ ] });
20588
+ }
19964
20589
  var ThinkingSection = ({ status }) => /* @__PURE__ */ jsxs(
19965
20590
  "div",
19966
20591
  {
@@ -20045,14 +20670,14 @@ var MessageItem = ({
20045
20670
  const { data: workspace } = useWorkspace(workspaceId);
20046
20671
  const chatbotName = getChatbotName(workspace?.name);
20047
20672
  const { addInputToMessageMutation } = useAddInputToMessage();
20048
- const onSubmitUserForm = (messageId) => (name, value) => {
20673
+ const onSubmitUserForm = (messageId, channels = {}) => (name, value) => {
20049
20674
  if (!threadId || !messageId) return;
20050
20675
  addInputToMessageMutation.mutate({
20051
20676
  threadId,
20052
20677
  messageId,
20053
20678
  name,
20054
20679
  value,
20055
- channels: {}
20680
+ channels
20056
20681
  });
20057
20682
  };
20058
20683
  const safeTime = (d) => {
@@ -20183,7 +20808,7 @@ var MessageItem = ({
20183
20808
  userOutput: v.value && typeof v.value === "object" ? v.value : null,
20184
20809
  chatbotName,
20185
20810
  userInput: userInput?.value,
20186
- onSubmitUserForm: onSubmitUserForm(message.id ?? "")
20811
+ onSubmitUserForm: onSubmitUserForm(message.id ?? "", v.channels)
20187
20812
  },
20188
20813
  `user-${message.id ?? "msg"}-${keyCounter++}`
20189
20814
  )
@@ -20248,22 +20873,26 @@ var MessageItem = ({
20248
20873
  );
20249
20874
  }
20250
20875
  (message.errors ?? []).forEach((error, errorIndex) => {
20876
+ const key = `error-${message.id ?? "msg"}-${errorIndex}-${error.errorCode ?? error.code}`;
20877
+ const detail = getMessageErrorDetail(error);
20251
20878
  bubbles.push(
20252
- /* @__PURE__ */ jsx(
20253
- MessageBubble,
20254
- {
20255
- createdBy: chatbotName,
20256
- createdAt: message.createdAt,
20257
- type: "Output" /* OUTPUT */,
20258
- content: [{ text: getMessageErrorText(error) }],
20259
- files: [],
20260
- sources: [],
20261
- chatbotName,
20262
- userOutput: null,
20263
- userInput: null
20264
- },
20265
- `error-${message.id ?? "msg"}-${errorIndex}-${error.errorCode ?? error.code}`
20266
- )
20879
+ /* @__PURE__ */ jsxs(Fragment$1, { children: [
20880
+ /* @__PURE__ */ jsx(
20881
+ MessageBubble,
20882
+ {
20883
+ createdBy: chatbotName,
20884
+ createdAt: message.createdAt,
20885
+ type: "Output" /* OUTPUT */,
20886
+ content: [{ text: getMessageErrorText(error) }],
20887
+ files: [],
20888
+ sources: [],
20889
+ chatbotName,
20890
+ userOutput: null,
20891
+ userInput: null
20892
+ }
20893
+ ),
20894
+ detail && /* @__PURE__ */ jsx(MessageErrorDetails, { detail })
20895
+ ] }, key)
20267
20896
  );
20268
20897
  });
20269
20898
  return bubbles;
@@ -20691,6 +21320,6 @@ function getUserPhotoUrl(userId) {
20691
21320
  return `${base2}/users/${userId}/photo`;
20692
21321
  }
20693
21322
 
20694
- 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 };
21323
+ 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, getMessageErrorDetail, 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 };
20695
21324
  //# sourceMappingURL=index.js.map
20696
21325
  //# sourceMappingURL=index.js.map