@smartspace/chat-ui 1.14.4-dev.88aea18 → 1.14.4-dev.b35149c

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, Cpu, ChevronDown, Check, Globe, Zap, SlidersHorizontal, X, Paperclip, Mic, Square, Send, Minimize2, AlertTriangle, FileImage, FileVideo, FileAudio, FileArchive, FileCode, FileSpreadsheet, Presentation, FileText, ChevronUp, ExternalLink, Download, ShieldAlert, Copy } from 'lucide-react';
4
- import * as React8 from 'react';
5
- import { createContext, forwardRef, useImperativeHandle, useRef, useState, useEffect, useMemo, useCallback, useContext, useSyncExternalStore } from '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
+ import * as React9 from 'react';
5
+ import React9__default, { createContext, forwardRef, useImperativeHandle, useRef, useState, useEffect, useMemo, useCallback, useContext, useSyncExternalStore } 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,14 +17,13 @@ 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';
24
24
  import { withJsonFormsControlProps, withJsonFormsLayoutProps, JsonForms, ResolvedJsonFormsDispatch } from '@jsonforms/react';
25
25
  import { rankWith, uiTypeIs, createAjv } from '@jsonforms/core';
26
26
  import { vanillaRenderers, vanillaCells } from '@jsonforms/vanilla-renderers';
27
- import { FormControl, InputLabel, Select, MenuItem, useTheme, Box, Skeleton as Skeleton$1 } from '@mui/material';
28
27
  import 'ace-builds/src-noconflict/ace';
29
28
  import 'ace-builds/src-noconflict/ext-language_tools';
30
29
  import 'ace-builds/src-noconflict/mode-json';
@@ -57,6 +56,7 @@ import relativeTime from 'dayjs/plugin/relativeTime';
57
56
  import utc from 'dayjs/plugin/utc';
58
57
  import { Slot } from '@radix-ui/react-slot';
59
58
  import { cva } from 'class-variance-authority';
59
+ import { useTheme, Box, Skeleton as Skeleton$1 } from '@mui/material';
60
60
 
61
61
  var __defProp = Object.defineProperty;
62
62
  var __export = (target, all5) => {
@@ -365,6 +365,19 @@ var MessageValueType = /* @__PURE__ */ ((MessageValueType2) => {
365
365
  MessageValueType2["INPUT"] = "Input";
366
366
  return MessageValueType2;
367
367
  })(MessageValueType || {});
368
+ var MessageResponseSourceType = /* @__PURE__ */ ((MessageResponseSourceType2) => {
369
+ MessageResponseSourceType2["BlobInternal"] = "BlobInternal";
370
+ MessageResponseSourceType2["WebExternal"] = "WebExternal";
371
+ MessageResponseSourceType2["File"] = "File";
372
+ MessageResponseSourceType2["URL"] = "URL";
373
+ return MessageResponseSourceType2;
374
+ })(MessageResponseSourceType || {});
375
+ var MessageAttribution = /* @__PURE__ */ ((MessageAttribution2) => {
376
+ MessageAttribution2["Supported"] = "supported";
377
+ MessageAttribution2["Partial"] = "partial";
378
+ MessageAttribution2["Unsupported"] = "unsupported";
379
+ return MessageAttribution2;
380
+ })(MessageAttribution || {});
368
381
 
369
382
  // src/domains/threads/queryKeys.ts
370
383
  var THREAD_LIST_PAGE_SIZE = 30;
@@ -587,9 +600,14 @@ var useThread = ({
587
600
  };
588
601
  var useThreadIsRunning = (workspaceId, threadId) => {
589
602
  const queryClient = useQueryClient();
603
+ const service = useChatService();
590
604
  const { data: detailThread } = useQuery({
591
- queryKey: threadsKeys.detail(workspaceId ?? "", threadId ?? ""),
592
- queryFn: skipToken
605
+ ...threadDetailOptions({
606
+ service,
607
+ workspaceId: workspaceId ?? "",
608
+ threadId: threadId ?? ""
609
+ }),
610
+ enabled: false
593
611
  });
594
612
  const listThread = workspaceId && threadId ? getThreadPlaceholderFromListCache(queryClient, workspaceId, threadId) : void 0;
595
613
  const { data: optimistic } = useQuery({
@@ -824,6 +842,37 @@ function useMessages(threadId) {
824
842
  });
825
843
  }
826
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
+
827
876
  // ../../node_modules/.pnpm/@milkdown+exception@7.20.0/node_modules/@milkdown/exception/lib/index.js
828
877
  var ErrorCode = /* @__PURE__ */ (function(ErrorCode2) {
829
878
  ErrorCode2["docTypeError"] = "docTypeError";
@@ -957,6 +1006,75 @@ var autolink = $prose(() => {
957
1006
  }
958
1007
  });
959
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
+ }
960
1078
 
961
1079
  // src/shared/markdown/extensions/fileTag.ts
962
1080
  var fileTag = $node("fileTag", () => ({
@@ -1866,7 +1984,7 @@ function EditorInner({
1866
1984
  } catch {
1867
1985
  }
1868
1986
  });
1869
- }).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);
1870
1988
  },
1871
1989
  [isEditable]
1872
1990
  );
@@ -2076,6 +2194,25 @@ function EditorInner({
2076
2194
  return value ?? "";
2077
2195
  }
2078
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
+ },
2079
2216
  clear: () => {
2080
2217
  const view = viewRef.current;
2081
2218
  if (!view) return;
@@ -2419,6 +2556,326 @@ var MarkdownEditor = forwardRef((props, ref) => {
2419
2556
  return /* @__PURE__ */ jsx(MilkdownProvider, { children: /* @__PURE__ */ jsx(EditorInner, { ...props, editorHandleRef: ref }) });
2420
2557
  });
2421
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
+ }
2422
2879
  function useFlowRunVariables(flowRunId) {
2423
2880
  const service = useChatService();
2424
2881
  return useQuery({
@@ -2430,6 +2887,31 @@ function useFlowRunVariables(flowRunId) {
2430
2887
  enabled: !!flowRunId
2431
2888
  });
2432
2889
  }
2890
+
2891
+ // src/chat-variables/renders/fieldStyles.ts
2892
+ var scaleFor = (surface) => surface === "form" ? "comfortable" : "dense";
2893
+ var fieldLabelClass = (scale) => `block truncate font-medium text-muted-foreground ${scale === "dense" ? "mb-1 text-xs" : "mb-1.5 text-sm"}`;
2894
+ var CONTROL_BASE = "rounded-md border bg-background text-foreground transition-colors placeholder:text-muted-foreground ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-60";
2895
+ var borderClass = (hasError) => hasError ? "border-destructive" : "border-input";
2896
+ var fieldControlClass = (scale, hasError) => `w-full ${CONTROL_BASE} ${borderClass(hasError)} ${scale === "dense" ? "px-2.5 py-1.5 text-xs" : "px-3 py-2 text-sm"}`;
2897
+ var fieldTriggerClass = (scale, hasError) => `w-full ${CONTROL_BASE} ${borderClass(
2898
+ hasError
2899
+ )} flex items-center justify-between gap-2 text-left ${scale === "dense" ? "h-8 px-2.5 text-xs" : "h-10 px-3 text-sm"}`;
2900
+ var fieldNumberClass = (scale, hasError) => `${CONTROL_BASE} ${borderClass(hasError)} tabular-nums ${scale === "dense" ? "h-8 w-20 px-2 text-xs" : "h-10 w-24 px-2.5 text-sm"}`;
2901
+ var fieldHintClass = "mt-1 text-xs text-muted-foreground";
2902
+ var fieldErrorClass = "mt-1 text-xs text-destructive";
2903
+ var fieldRowClass = "flex items-center justify-between gap-3";
2904
+ var fieldRowLabelClass = (scale) => `min-w-0 flex-1 truncate font-medium text-muted-foreground ${scale === "dense" ? "text-xs" : "text-sm"}`;
2905
+ var pillClass = (opts) => `flex h-8 shrink-0 items-center gap-1.5 rounded-full border px-2.5 text-xs font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary ${opts.active ? "border-primary/30 bg-primary/10 text-primary" : "border-border/70 bg-transparent text-muted-foreground hover:bg-secondary"} ${opts.hasError ? "border-destructive text-destructive" : ""} ${opts.disabled ? "cursor-not-allowed opacity-60" : ""}`;
2906
+ var pillLabelClass = "max-w-[9rem] truncate";
2907
+ var pillValueClass = "max-w-[9rem] truncate text-foreground";
2908
+ var switchTrackClass = (on, disabled) => `peer inline-flex h-6 w-11 shrink-0 items-center rounded-full border-2 border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background ${on ? "bg-primary" : "bg-input"} ${disabled ? "cursor-not-allowed opacity-50" : "cursor-pointer"}`;
2909
+ var switchThumbClass = (on) => `pointer-events-none block h-5 w-5 rounded-full bg-background shadow-lg ring-0 transition-transform ${on ? "translate-x-5" : "translate-x-0"}`;
2910
+ var POPOVER_WIDTH_PX = 288;
2911
+ var POPOVER_MAX_HEIGHT_PX = 288;
2912
+ var popoverClass = "flex flex-col overflow-hidden rounded-xl border border-border bg-popover shadow-lg";
2913
+ var popoverRowClass = "flex w-full items-center gap-2.5 px-3 py-2 text-left transition-colors hover:bg-secondary/60";
2914
+ var popoverHeadingClass = "text-[10px] font-semibold uppercase tracking-wide text-muted-foreground";
2433
2915
  function iconFor(label) {
2434
2916
  const name = (label ?? "").toLowerCase();
2435
2917
  if (/search|web|browse|internet/.test(name)) return Globe;
@@ -2445,7 +2927,8 @@ var BooleanRenderer = ({
2445
2927
  errors,
2446
2928
  uischema,
2447
2929
  visible,
2448
- enabled
2930
+ enabled,
2931
+ config
2449
2932
  }) => {
2450
2933
  const onToggle = useCallback(() => {
2451
2934
  handleChange(path2, !data);
@@ -2456,13 +2939,17 @@ var BooleanRenderer = ({
2456
2939
  const hasError = !!errors && errors.length > 0;
2457
2940
  const isChecked = Boolean(data);
2458
2941
  const Icon = iconFor(label);
2942
+ const surface = (config ?? {}).surface ?? "form";
2943
+ const scale = scaleFor(surface);
2459
2944
  const tooltip = [label, description, hasError ? errors : null].filter(Boolean).join(" \u2014 ");
2460
- return /* @__PURE__ */ jsxs(
2945
+ const toggle = /* @__PURE__ */ jsx(
2461
2946
  "button",
2462
2947
  {
2463
2948
  id: `toggle-${path2}`,
2464
2949
  type: "button",
2465
- "aria-pressed": isChecked,
2950
+ role: surface === "bar" ? void 0 : "switch",
2951
+ "aria-pressed": surface === "bar" ? isChecked : void 0,
2952
+ "aria-checked": surface === "bar" ? void 0 : isChecked,
2466
2953
  "aria-label": label,
2467
2954
  title: tooltip || void 0,
2468
2955
  onClick: onToggle,
@@ -2472,16 +2959,28 @@ var BooleanRenderer = ({
2472
2959
  e.currentTarget.blur();
2473
2960
  }
2474
2961
  },
2475
- className: `boolean-switch flex h-8 shrink-0 items-center gap-1.5 rounded-full border px-2.5 text-xs font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary ${isChecked ? "border-primary/30 bg-primary/10 text-primary" : "border-transparent bg-transparent text-muted-foreground hover:bg-secondary"} ${hasError ? "border-destructive text-destructive" : ""} ${isDisabled ? "cursor-not-allowed opacity-60" : ""}`,
2476
- children: [
2962
+ className: surface === "bar" ? `boolean-switch ${pillClass({
2963
+ active: isChecked,
2964
+ hasError,
2965
+ disabled: isDisabled
2966
+ })}` : switchTrackClass(isChecked, isDisabled),
2967
+ children: surface === "bar" ? /* @__PURE__ */ jsxs(Fragment, { children: [
2477
2968
  /* @__PURE__ */ jsx(Icon, { className: "h-3.5 w-3.5 shrink-0" }),
2478
2969
  label && // Desktop-first: the app and the package each ship a full Tailwind
2479
- // build, so a base `hidden` can out-order `sm:inline`. `max-sm:hidden`
2480
- // is the form that survives both cascades.
2481
- /* @__PURE__ */ jsx("span", { className: "inline max-w-[9rem] truncate max-sm:hidden", children: label })
2482
- ]
2970
+ // build, so a base `hidden` can out-order `sm:inline`.
2971
+ // `max-sm:hidden` is the form that survives both cascades.
2972
+ /* @__PURE__ */ jsx("span", { className: `inline ${pillLabelClass} max-sm:hidden`, children: label })
2973
+ ] }) : /* @__PURE__ */ jsx("span", { className: switchThumbClass(isChecked) })
2483
2974
  }
2484
2975
  );
2976
+ if (surface === "bar") return toggle;
2977
+ return /* @__PURE__ */ jsxs("div", { className: "ss-jsonforms-field ss-jsonforms-boolean", children: [
2978
+ /* @__PURE__ */ jsxs("div", { className: fieldRowClass, children: [
2979
+ /* @__PURE__ */ jsx("label", { htmlFor: `toggle-${path2}`, className: fieldRowLabelClass(scale), children: label }),
2980
+ toggle
2981
+ ] }),
2982
+ hasError && /* @__PURE__ */ jsx("div", { className: fieldErrorClass, children: errors })
2983
+ ] });
2485
2984
  };
2486
2985
  var booleanRendererTester = rankWith(40, (uischema, schema) => {
2487
2986
  if (uischema.type !== "Control") return false;
@@ -2491,6 +2990,104 @@ var booleanRendererTester = rankWith(40, (uischema, schema) => {
2491
2990
  return fieldSchema?.type === "boolean";
2492
2991
  });
2493
2992
  var BooleanRendererControl = withJsonFormsControlProps(BooleanRenderer);
2993
+ var GAP_PX = 8;
2994
+ var MIN_TRIGGER_WIDTH_PX = 224;
2995
+ var MIN_PANEL_HEIGHT_PX = 180;
2996
+ var NestedPopoverContext = React9__default.createContext(null);
2997
+ function useAnchoredPopover({
2998
+ trigger,
2999
+ popover,
3000
+ width,
3001
+ maxHeight,
3002
+ role,
3003
+ label
3004
+ }) {
3005
+ const [isOpen, setIsOpen] = useState(false);
3006
+ const parentRegistry = useContext(NestedPopoverContext);
3007
+ const descendants = useRef(/* @__PURE__ */ new Set());
3008
+ const close = useCallback(() => setIsOpen(false), []);
3009
+ const toggle = useCallback(() => setIsOpen((open) => !open), []);
3010
+ const registry = useMemo(
3011
+ () => ({
3012
+ register: (el) => {
3013
+ descendants.current.add(el);
3014
+ parentRegistry?.register(el);
3015
+ },
3016
+ unregister: (el) => {
3017
+ descendants.current.delete(el);
3018
+ parentRegistry?.unregister(el);
3019
+ }
3020
+ }),
3021
+ [parentRegistry]
3022
+ );
3023
+ useEffect(() => {
3024
+ if (!isOpen || !parentRegistry) return;
3025
+ const el = popover.current;
3026
+ if (!el) return;
3027
+ parentRegistry.register(el);
3028
+ return () => parentRegistry.unregister(el);
3029
+ }, [isOpen, parentRegistry, popover]);
3030
+ useEffect(() => {
3031
+ if (!isOpen) return;
3032
+ const onPointerDown = (event) => {
3033
+ const target = event.target;
3034
+ if (trigger.current?.contains(target)) return;
3035
+ if (popover.current?.contains(target)) return;
3036
+ for (const el of descendants.current) {
3037
+ if (el.contains(target)) return;
3038
+ }
3039
+ close();
3040
+ };
3041
+ const onKeyDown = (event) => {
3042
+ if (event.key === "Escape") close();
3043
+ };
3044
+ document.addEventListener("mousedown", onPointerDown);
3045
+ document.addEventListener("keydown", onKeyDown);
3046
+ window.addEventListener("resize", close);
3047
+ return () => {
3048
+ document.removeEventListener("mousedown", onPointerDown);
3049
+ document.removeEventListener("keydown", onKeyDown);
3050
+ window.removeEventListener("resize", close);
3051
+ };
3052
+ }, [isOpen, close, trigger, popover]);
3053
+ const renderPopover = (children2) => {
3054
+ if (!isOpen) return null;
3055
+ const anchor = trigger.current?.getBoundingClientRect();
3056
+ if (!anchor) return null;
3057
+ const panelWidth = width === "trigger" ? Math.max(anchor.width, MIN_TRIGGER_WIDTH_PX) : width;
3058
+ const spaceAbove = anchor.top - GAP_PX * 2;
3059
+ const spaceBelow = window.innerHeight - anchor.bottom - GAP_PX * 2;
3060
+ const openAbove = spaceAbove >= MIN_PANEL_HEIGHT_PX || spaceAbove >= spaceBelow;
3061
+ const panelMaxHeight = Math.max(
3062
+ 0,
3063
+ Math.min(maxHeight, openAbove ? spaceAbove : spaceBelow)
3064
+ );
3065
+ return createPortal(
3066
+ /* @__PURE__ */ jsx(
3067
+ "div",
3068
+ {
3069
+ ref: popover,
3070
+ role,
3071
+ "aria-label": label,
3072
+ className: `fixed z-50 ${popoverClass}`,
3073
+ style: {
3074
+ width: panelWidth,
3075
+ maxWidth: `calc(100vw - ${GAP_PX * 4}px)`,
3076
+ left: Math.max(
3077
+ GAP_PX,
3078
+ Math.min(anchor.left, window.innerWidth - panelWidth - GAP_PX)
3079
+ ),
3080
+ ...openAbove ? { bottom: window.innerHeight - anchor.top + GAP_PX } : { top: anchor.bottom + GAP_PX },
3081
+ maxHeight: panelMaxHeight
3082
+ },
3083
+ children: /* @__PURE__ */ jsx(NestedPopoverContext.Provider, { value: registry, children: children2 })
3084
+ }
3085
+ ),
3086
+ document.body
3087
+ );
3088
+ };
3089
+ return { isOpen, close, toggle, renderPopover };
3090
+ }
2494
3091
  function hasConst(x) {
2495
3092
  return !!x && typeof x === "object" && "const" in x;
2496
3093
  }
@@ -2529,185 +3126,109 @@ var DropdownRenderer = ({
2529
3126
  label,
2530
3127
  description,
2531
3128
  errors,
2532
- uischema
3129
+ config,
3130
+ uischema,
3131
+ visible
2533
3132
  }) => {
3133
+ const triggerRef = useRef(null);
3134
+ const listRef = useRef(null);
3135
+ const surface = (config ?? {}).surface ?? "form";
3136
+ const scale = scaleFor(surface);
3137
+ const { isOpen, close, toggle, renderPopover } = useAnchoredPopover({
3138
+ trigger: triggerRef,
3139
+ popover: listRef,
3140
+ // A select's list lines up with its own control.
3141
+ width: "trigger",
3142
+ maxHeight: POPOVER_MAX_HEIGHT_PX,
3143
+ role: "listbox",
3144
+ label: label || "Options"
3145
+ });
2534
3146
  const options = toOptions(schema);
2535
- const handleSelectionChange = useCallback(
2536
- (event) => {
2537
- handleChange(path2, event.target.value);
3147
+ const select = useCallback(
3148
+ (value) => {
3149
+ handleChange(path2, value);
3150
+ close();
2538
3151
  },
2539
- [handleChange, path2]
3152
+ [handleChange, path2, close]
2540
3153
  );
2541
- const displayValue = data ?? "";
3154
+ if (!visible) return null;
2542
3155
  const readOnly = uischema?.access === "Read";
2543
3156
  const isDisabled = !enabled || readOnly;
2544
- return /* @__PURE__ */ jsxs(
2545
- FormControl,
3157
+ const hasError = !!errors;
3158
+ const selected = options.find((option) => option.const === data);
3159
+ const valueText = selected ? selected.title ?? String(selected.const) : data != null && data !== "" ? String(data) : "";
3160
+ const triggerId = `${path2}-trigger`;
3161
+ const isBar = surface === "bar";
3162
+ const placeholder = `Select ${(label || "value").toLowerCase()}\u2026`;
3163
+ const trigger = /* @__PURE__ */ jsxs(
3164
+ "button",
2546
3165
  {
2547
- variant: "outlined",
2548
- size: "small",
2549
- fullWidth: true,
2550
- error: !!errors,
3166
+ id: triggerId,
3167
+ ref: triggerRef,
3168
+ type: "button",
2551
3169
  disabled: isDisabled,
2552
- className: "compact-field",
2553
- sx: {
2554
- minWidth: "220px"
2555
- },
3170
+ onClick: toggle,
3171
+ "aria-haspopup": "listbox",
3172
+ "aria-expanded": isOpen,
3173
+ "aria-label": label,
3174
+ title: [label, valueText, hasError ? errors : null].filter(Boolean).join(" \u2014 ") || void 0,
3175
+ className: isBar ? `ss-jsonforms-select-trigger ${pillClass({
3176
+ active: isOpen,
3177
+ hasError,
3178
+ disabled: isDisabled
3179
+ })}` : `ss-jsonforms-select-trigger ${fieldTriggerClass(scale, hasError)}`,
2556
3180
  children: [
3181
+ isBar && label && /* @__PURE__ */ jsx("span", { className: `inline ${pillLabelClass} max-sm:hidden`, children: label }),
2557
3182
  /* @__PURE__ */ jsx(
2558
- InputLabel,
3183
+ "span",
2559
3184
  {
2560
- id: `${path2}-label`,
2561
- sx: {
2562
- color: "#6b7280",
2563
- fontSize: "0.875rem",
2564
- fontWeight: 500,
2565
- "&.Mui-focused": {
2566
- color: "#6366f1"
2567
- },
2568
- "&.Mui-error": {
2569
- color: "#ef4444"
2570
- }
2571
- },
2572
- children: label
3185
+ className: isBar ? pillValueClass : `min-w-0 flex-1 truncate ${valueText ? "" : "text-muted-foreground"}`,
3186
+ children: valueText || (isBar ? "\u2014" : placeholder)
2573
3187
  }
2574
3188
  ),
2575
3189
  /* @__PURE__ */ jsx(
2576
- Select,
2577
- {
2578
- labelId: `${path2}-label`,
2579
- value: displayValue,
2580
- onChange: handleSelectionChange,
2581
- label,
2582
- sx: {
2583
- "& .MuiOutlinedInput-root": {
2584
- backgroundColor: "#fafafa",
2585
- borderRadius: "8px",
2586
- transition: "all 0.2s ease-in-out",
2587
- height: "40px",
2588
- // Same height as model dropdown
2589
- "& fieldset": {
2590
- borderColor: "#e5e7eb",
2591
- borderWidth: "1px"
2592
- },
2593
- "&:hover": {
2594
- backgroundColor: "#ffffff",
2595
- "& fieldset": {
2596
- borderColor: "#9ca3af"
2597
- }
2598
- },
2599
- "&.Mui-focused": {
2600
- backgroundColor: "#ffffff",
2601
- "& fieldset": {
2602
- borderColor: "#6366f1",
2603
- borderWidth: "2px"
2604
- }
2605
- },
2606
- "&.Mui-error": {
2607
- "& fieldset": {
2608
- borderColor: "#ef4444"
2609
- }
2610
- }
2611
- },
2612
- "& .MuiSelect-select": {
2613
- backgroundColor: "#fafafa",
2614
- borderRadius: "8px",
2615
- transition: "all 0.2s ease-in-out",
2616
- height: "40px",
2617
- display: "flex",
2618
- alignItems: "center",
2619
- paddingRight: "32px !important",
2620
- "&:hover": {
2621
- backgroundColor: "#ffffff"
2622
- },
2623
- "&.Mui-focused": {
2624
- backgroundColor: "#ffffff"
2625
- }
2626
- },
2627
- "& .MuiOutlinedInput-notchedOutline": {
2628
- borderColor: "#e5e7eb",
2629
- borderWidth: "1px",
2630
- borderRadius: "8px"
2631
- },
2632
- "&:hover .MuiOutlinedInput-notchedOutline": {
2633
- borderColor: "#9ca3af"
2634
- },
2635
- "&.Mui-focused .MuiOutlinedInput-notchedOutline": {
2636
- borderColor: "#6366f1",
2637
- borderWidth: "2px"
2638
- },
2639
- "&.Mui-error .MuiOutlinedInput-notchedOutline": {
2640
- borderColor: "#ef4444"
2641
- },
2642
- "& .MuiSelect-icon": {
2643
- color: "#9ca3af",
2644
- "&:hover": {
2645
- color: "#6b7280"
2646
- }
2647
- }
2648
- },
2649
- MenuProps: {
2650
- PaperProps: {
2651
- sx: {
2652
- borderRadius: "8px",
2653
- boxShadow: "0 10px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04)",
2654
- border: "1px solid #e5e7eb",
2655
- marginTop: "4px",
2656
- maxHeight: "280px"
2657
- }
2658
- },
2659
- MenuListProps: {
2660
- sx: {
2661
- padding: 0
2662
- }
2663
- }
2664
- },
2665
- children: options.map((option, index) => {
2666
- const raw2 = option.const;
2667
- const key = typeof raw2 === "string" || typeof raw2 === "number" ? String(raw2) : String(index);
2668
- const value = typeof raw2 === "string" || typeof raw2 === "number" ? raw2 : String(raw2 ?? index);
2669
- return /* @__PURE__ */ jsx(
2670
- MenuItem,
2671
- {
2672
- value,
2673
- sx: {
2674
- padding: "12px 16px",
2675
- fontSize: "0.875rem",
2676
- borderBottom: "1px solid #f3f4f6",
2677
- "&:last-child": {
2678
- borderBottom: "none"
2679
- },
2680
- "&:hover": {
2681
- backgroundColor: "#f8fafc"
2682
- },
2683
- "&.Mui-selected": {
2684
- backgroundColor: "#eff6ff",
2685
- "&:hover": {
2686
- backgroundColor: "#dbeafe"
2687
- }
2688
- }
2689
- },
2690
- children: option.title ?? String(raw2)
2691
- },
2692
- key
2693
- );
2694
- })
2695
- }
2696
- ),
2697
- (description || errors) && /* @__PURE__ */ jsx(
2698
- "div",
3190
+ ChevronDown,
2699
3191
  {
2700
- style: {
2701
- fontSize: "0.75rem",
2702
- marginTop: "4px",
2703
- color: errors ? "#ef4444" : "#6b7280"
2704
- },
2705
- children: errors || description
3192
+ className: `shrink-0 opacity-50 ${isBar ? "h-3 w-3" : "h-4 w-4"}`
2706
3193
  }
2707
3194
  )
2708
3195
  ]
2709
3196
  }
2710
3197
  );
3198
+ return /* @__PURE__ */ jsxs("div", { className: "ss-jsonforms-field ss-jsonforms-select compact-field", children: [
3199
+ !isBar && label && /* @__PURE__ */ jsx("label", { htmlFor: triggerId, className: fieldLabelClass(scale), children: label }),
3200
+ trigger,
3201
+ !isBar && (errors || description) && /* @__PURE__ */ jsx("div", { className: errors ? fieldErrorClass : fieldHintClass, children: errors || description }),
3202
+ renderPopover(
3203
+ /* @__PURE__ */ jsxs("div", { className: "min-h-0 overflow-y-auto py-1", children: [
3204
+ options.length === 0 && /* @__PURE__ */ jsx("p", { className: "px-3 py-6 text-center text-xs text-muted-foreground", children: "No options available" }),
3205
+ options.map((option, index) => {
3206
+ const active = option.const === data;
3207
+ const text6 = option.title ?? String(option.const);
3208
+ return /* @__PURE__ */ jsxs(
3209
+ "button",
3210
+ {
3211
+ type: "button",
3212
+ role: "option",
3213
+ "aria-selected": active,
3214
+ onClick: () => select(option.const),
3215
+ className: popoverRowClass,
3216
+ children: [
3217
+ /* @__PURE__ */ jsx(
3218
+ Check,
3219
+ {
3220
+ className: `h-3.5 w-3.5 shrink-0 ${active ? "text-primary" : "opacity-0"}`
3221
+ }
3222
+ ),
3223
+ /* @__PURE__ */ jsx("span", { className: "min-w-0 flex-1 truncate text-sm text-foreground", children: text6 })
3224
+ ]
3225
+ },
3226
+ typeof option.const === "string" || typeof option.const === "number" ? String(option.const) : `option-${index}`
3227
+ );
3228
+ })
3229
+ ] })
3230
+ )
3231
+ ] });
2711
3232
  };
2712
3233
  var dropdownRendererTester = rankWith(
2713
3234
  90,
@@ -2725,8 +3246,8 @@ var dropdownRendererTester = rankWith(
2725
3246
  return false;
2726
3247
  }
2727
3248
  const hasDropdownOptions = !!(fieldSchema.oneOf || fieldSchema.anyOf || fieldSchema.enum && Array.isArray(fieldSchema.enum));
2728
- const isModelSelector = typeof fieldSchema === "object" && fieldSchema && fieldSchema["x-model-selector"] === true || fieldSchema.title === "ModelId" || fieldSchema.format === "uuid";
2729
- return hasDropdownOptions && !isModelSelector;
3249
+ const isModelSelector2 = typeof fieldSchema === "object" && fieldSchema && fieldSchema["x-model-selector"] === true || fieldSchema.title === "ModelId" || fieldSchema.format === "uuid";
3250
+ return hasDropdownOptions && !isModelSelector2;
2730
3251
  }
2731
3252
  );
2732
3253
  var DropdownRendererControl = withJsonFormsControlProps(DropdownRenderer);
@@ -2761,7 +3282,8 @@ var JsonEditorRenderer = ({
2761
3282
  uischema,
2762
3283
  visible,
2763
3284
  enabled,
2764
- required
3285
+ required,
3286
+ config
2765
3287
  }) => {
2766
3288
  const [jsonValue, setJsonValue] = useState("");
2767
3289
  const [displayedParseError, setDisplayedParseError] = useState(
@@ -2815,43 +3337,19 @@ var JsonEditorRenderer = ({
2815
3337
  }
2816
3338
  const readOnly = uischema?.access === "Read";
2817
3339
  const isDisabled = !enabled || readOnly;
3340
+ const scale = scaleFor(
3341
+ (config ?? {}).surface ?? "form"
3342
+ );
2818
3343
  return /* @__PURE__ */ jsxs("div", { style: { marginBottom: "1rem" }, children: [
2819
- label && /* @__PURE__ */ jsxs(
2820
- "label",
2821
- {
2822
- style: {
2823
- display: "block",
2824
- color: "#475569",
2825
- fontSize: "0.875rem",
2826
- fontWeight: 500,
2827
- marginBottom: "0.375rem"
2828
- },
2829
- children: [
2830
- label,
2831
- required && /* @__PURE__ */ jsx("span", { style: { color: "#ef4444", marginLeft: "0.25rem" }, children: "*" })
2832
- ]
2833
- }
2834
- ),
2835
- description && /* @__PURE__ */ jsx(
2836
- "div",
2837
- {
2838
- style: {
2839
- color: "#6b7280",
2840
- fontSize: "0.75rem",
2841
- marginBottom: "0.5rem"
2842
- },
2843
- children: description
2844
- }
2845
- ),
3344
+ label && /* @__PURE__ */ jsxs("label", { className: fieldLabelClass(scale), children: [
3345
+ label,
3346
+ required && /* @__PURE__ */ jsx("span", { className: "ml-1 text-destructive", children: "*" })
3347
+ ] }),
3348
+ description && /* @__PURE__ */ jsx("div", { className: fieldHintClass, children: description }),
2846
3349
  /* @__PURE__ */ jsx(
2847
3350
  "div",
2848
3351
  {
2849
- style: {
2850
- border: errors || displayedParseError ? "1px solid #ef4444" : "1px solid #d1d5db",
2851
- borderRadius: "6px",
2852
- overflow: "hidden",
2853
- opacity: isDisabled ? 0.6 : 1
2854
- },
3352
+ className: `overflow-hidden rounded-md border ${errors || displayedParseError ? "border-destructive" : "border-input"} ${isDisabled ? "opacity-60" : ""}`,
2855
3353
  children: /* @__PURE__ */ jsx(
2856
3354
  AceEditor,
2857
3355
  {
@@ -2884,28 +3382,8 @@ var JsonEditorRenderer = ({
2884
3382
  )
2885
3383
  }
2886
3384
  ),
2887
- displayedParseError && /* @__PURE__ */ jsx(
2888
- "div",
2889
- {
2890
- style: {
2891
- color: "#ef4444",
2892
- fontSize: "0.75rem",
2893
- marginTop: "0.25rem"
2894
- },
2895
- children: displayedParseError
2896
- }
2897
- ),
2898
- errors && /* @__PURE__ */ jsx(
2899
- "div",
2900
- {
2901
- style: {
2902
- color: "#ef4444",
2903
- fontSize: "0.75rem",
2904
- marginTop: "0.25rem"
2905
- },
2906
- children: errors
2907
- }
2908
- )
3385
+ displayedParseError && /* @__PURE__ */ jsx("div", { className: fieldErrorClass, children: displayedParseError }),
3386
+ errors && /* @__PURE__ */ jsx("div", { className: fieldErrorClass, children: errors })
2909
3387
  ] });
2910
3388
  };
2911
3389
  var jsonEditorTester = rankWith(
@@ -2996,14 +3474,31 @@ var ModelIdRenderer = ({
2996
3474
  description,
2997
3475
  errors,
2998
3476
  uischema,
2999
- visible
3477
+ visible,
3478
+ config
3000
3479
  }) => {
3001
- const [isOpen, setIsOpen] = useState(false);
3002
3480
  const [searchValue, setSearchValue] = useState("");
3003
3481
  const [debouncedSearchValue, setDebouncedSearchValue] = useState("");
3004
- const containerRef = useRef(null);
3482
+ const triggerRef = useRef(null);
3005
3483
  const menuRef = useRef(null);
3006
3484
  const searchRef = useRef(null);
3485
+ const surface = (config ?? {}).surface ?? "bar";
3486
+ const scale = scaleFor(surface);
3487
+ const isBar = surface === "bar";
3488
+ const {
3489
+ isOpen,
3490
+ close: closeMenu,
3491
+ toggle,
3492
+ renderPopover
3493
+ } = useAnchoredPopover({
3494
+ trigger: triggerRef,
3495
+ popover: menuRef,
3496
+ // Wide enough for a provider glyph, a model name and its id underneath.
3497
+ width: POPOVER_WIDTH_PX,
3498
+ maxHeight: POPOVER_MAX_HEIGHT_PX,
3499
+ role: "listbox",
3500
+ label: "Models"
3501
+ });
3007
3502
  const debounceTimerRef = useRef(
3008
3503
  void 0
3009
3504
  );
@@ -3042,35 +3537,13 @@ var ModelIdRenderer = ({
3042
3537
  handleChange(path2, model.id);
3043
3538
  setSearchValue("");
3044
3539
  setDebouncedSearchValue("");
3045
- setIsOpen(false);
3540
+ closeMenu();
3046
3541
  },
3047
- [handleChange, path2]
3542
+ [handleChange, path2, closeMenu]
3048
3543
  );
3049
- const close = useCallback(() => {
3050
- setIsOpen(false);
3051
- setSearchValue("");
3052
- }, []);
3053
3544
  useEffect(() => {
3054
- if (!isOpen) return;
3055
- const onPointerDown = (event) => {
3056
- const target = event.target;
3057
- if (containerRef.current && !containerRef.current.contains(target) && !(menuRef.current && menuRef.current.contains(target))) {
3058
- close();
3059
- }
3060
- };
3061
- const onKeyDown = (event) => {
3062
- if (event.key === "Escape") close();
3063
- };
3064
- const onResize = () => close();
3065
- document.addEventListener("mousedown", onPointerDown);
3066
- document.addEventListener("keydown", onKeyDown);
3067
- window.addEventListener("resize", onResize);
3068
- return () => {
3069
- document.removeEventListener("mousedown", onPointerDown);
3070
- document.removeEventListener("keydown", onKeyDown);
3071
- window.removeEventListener("resize", onResize);
3072
- };
3073
- }, [isOpen, close]);
3545
+ if (!isOpen) setSearchValue("");
3546
+ }, [isOpen]);
3074
3547
  useEffect(() => {
3075
3548
  if (isOpen) searchRef.current?.focus();
3076
3549
  }, [isOpen]);
@@ -3079,53 +3552,58 @@ var ModelIdRenderer = ({
3079
3552
  const isDisabled = !enabled || readOnly;
3080
3553
  const hasError = !!errors && errors.length > 0;
3081
3554
  const selectedName = selectedModel ? selectedModel.displayName || selectedModel.name || "" : "";
3082
- const triggerText = selectedName || label || "Select model";
3555
+ const triggerText = selectedName || "Select model";
3083
3556
  const iconSrc = getModelIcon(selectedModel);
3084
3557
  const tooltip = [selectedName || label, description, hasError ? errors : null].filter(Boolean).join(" \u2014 ");
3085
- const anchorRect = isOpen ? containerRef.current?.getBoundingClientRect() : void 0;
3086
- return /* @__PURE__ */ jsxs("div", { className: "relative shrink-0", ref: containerRef, children: [
3087
- /* @__PURE__ */ jsxs(
3088
- "button",
3089
- {
3090
- type: "button",
3091
- onClick: () => setIsOpen((v) => !v),
3092
- disabled: isDisabled,
3093
- "aria-expanded": isOpen,
3094
- "aria-haspopup": "listbox",
3095
- "aria-label": label || "Select model",
3096
- title: tooltip || void 0,
3097
- className: `flex h-8 shrink-0 items-center gap-1.5 rounded-full border px-2.5 text-xs font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary ${isOpen ? "border-primary/30 bg-primary/10 text-primary" : "border-border/70 bg-transparent text-muted-foreground hover:bg-secondary"} ${hasError ? "border-destructive text-destructive" : ""} ${isDisabled ? "cursor-not-allowed opacity-60" : ""}`,
3098
- children: [
3099
- iconSrc ? /* @__PURE__ */ jsx(
3100
- "img",
3101
- {
3102
- src: iconSrc,
3103
- alt: "",
3104
- className: "h-3.5 w-3.5 shrink-0 object-contain"
3105
- }
3106
- ) : /* @__PURE__ */ jsx(Cpu, { className: "h-3.5 w-3.5 shrink-0" }),
3107
- /* @__PURE__ */ jsx("span", { className: "inline max-w-[9rem] truncate max-sm:hidden", children: triggerText }),
3108
- /* @__PURE__ */ jsx(ChevronDown, { className: "h-3 w-3 shrink-0 opacity-60" })
3109
- ]
3110
- }
3111
- ),
3112
- isOpen && anchorRect && createPortal(
3113
- /* @__PURE__ */ jsxs(
3114
- "div",
3115
- {
3116
- ref: menuRef,
3117
- role: "listbox",
3118
- "aria-label": label || "Models",
3119
- className: "fixed z-50 w-72 max-w-[calc(100vw-2rem)] overflow-hidden rounded-xl border border-border bg-popover shadow-lg",
3120
- style: {
3121
- left: Math.max(
3122
- 8,
3123
- Math.min(anchorRect.left, window.innerWidth - 288 - 8)
3124
- ),
3125
- bottom: window.innerHeight - anchorRect.top + 8
3126
- },
3127
- children: [
3128
- /* @__PURE__ */ jsx("div", { className: "border-b border-border p-2", children: /* @__PURE__ */ jsx(
3558
+ const triggerLabel = !label || label === "ModelId" ? "Model" : label;
3559
+ const trigger = /* @__PURE__ */ jsxs(
3560
+ "button",
3561
+ {
3562
+ ref: triggerRef,
3563
+ id: `${path2}-trigger`,
3564
+ type: "button",
3565
+ onClick: toggle,
3566
+ disabled: isDisabled,
3567
+ "aria-expanded": isOpen,
3568
+ "aria-haspopup": "listbox",
3569
+ "aria-label": triggerLabel,
3570
+ title: tooltip || void 0,
3571
+ className: isBar ? pillClass({ active: isOpen, hasError, disabled: isDisabled }) : fieldTriggerClass(scale, hasError),
3572
+ children: [
3573
+ iconSrc ? /* @__PURE__ */ jsx(
3574
+ "img",
3575
+ {
3576
+ src: iconSrc,
3577
+ alt: "",
3578
+ className: "h-3.5 w-3.5 shrink-0 object-contain"
3579
+ }
3580
+ ) : /* @__PURE__ */ jsx(Cpu, { className: "h-3.5 w-3.5 shrink-0" }),
3581
+ /* @__PURE__ */ jsx(
3582
+ "span",
3583
+ {
3584
+ className: isBar ? `inline ${pillValueClass} max-sm:hidden` : "min-w-0 flex-1 truncate",
3585
+ children: triggerText
3586
+ }
3587
+ ),
3588
+ /* @__PURE__ */ jsx(
3589
+ ChevronDown,
3590
+ {
3591
+ className: `shrink-0 opacity-60 ${isBar ? "h-3 w-3" : "h-4 w-4"}`
3592
+ }
3593
+ )
3594
+ ]
3595
+ }
3596
+ );
3597
+ return /* @__PURE__ */ jsxs(
3598
+ "div",
3599
+ {
3600
+ className: isBar ? "ss-jsonforms-field ss-jsonforms-model relative shrink-0" : "ss-jsonforms-field ss-jsonforms-model compact-field",
3601
+ children: [
3602
+ !isBar && /* @__PURE__ */ jsx("label", { htmlFor: `${path2}-trigger`, className: fieldLabelClass(scale), children: triggerLabel }),
3603
+ trigger,
3604
+ renderPopover(
3605
+ /* @__PURE__ */ jsxs(Fragment, { children: [
3606
+ /* @__PURE__ */ jsx("div", { className: "shrink-0 border-b border-border p-2", children: /* @__PURE__ */ jsx(
3129
3607
  "input",
3130
3608
  {
3131
3609
  ref: searchRef,
@@ -3137,7 +3615,7 @@ var ModelIdRenderer = ({
3137
3615
  className: "h-8 w-full rounded-md border border-border bg-background px-2 text-xs text-foreground outline-none placeholder:text-muted-foreground focus:border-primary/40"
3138
3616
  }
3139
3617
  ) }),
3140
- /* @__PURE__ */ jsxs("div", { className: "max-h-72 overflow-y-auto", children: [
3618
+ /* @__PURE__ */ jsxs("div", { className: "min-h-0 overflow-y-auto", children: [
3141
3619
  isLoading && listModels.length === 0 && /* @__PURE__ */ jsx("div", { className: "flex items-center justify-center py-6", children: /* @__PURE__ */ jsx(Loader2, { className: "h-4 w-4 animate-spin text-primary" }) }),
3142
3620
  !isLoading && listModels.length === 0 && /* @__PURE__ */ jsx("p", { className: "px-3 py-6 text-center text-xs text-muted-foreground", children: searchValue ? "No models found" : "No models available" }),
3143
3621
  listModels.map((model) => {
@@ -3150,7 +3628,7 @@ var ModelIdRenderer = ({
3150
3628
  role: "option",
3151
3629
  "aria-selected": active,
3152
3630
  onClick: () => handleSelect(model),
3153
- className: "flex w-full items-center gap-2.5 px-3 py-2 text-left transition-colors hover:bg-secondary/60",
3631
+ className: popoverRowClass,
3154
3632
  children: [
3155
3633
  /* @__PURE__ */ jsx(
3156
3634
  Check,
@@ -3176,12 +3654,11 @@ var ModelIdRenderer = ({
3176
3654
  );
3177
3655
  })
3178
3656
  ] })
3179
- ]
3180
- }
3181
- ),
3182
- document.body
3183
- )
3184
- ] });
3657
+ ] })
3658
+ )
3659
+ ]
3660
+ }
3661
+ );
3185
3662
  };
3186
3663
  var modelIdRendererTester = rankWith(
3187
3664
  100,
@@ -3216,6 +3693,7 @@ var NumberRenderer = ({
3216
3693
  uischema,
3217
3694
  visible,
3218
3695
  enabled,
3696
+ config,
3219
3697
  required
3220
3698
  }) => {
3221
3699
  const isInteger = schema?.type === "integer";
@@ -3244,40 +3722,58 @@ var NumberRenderer = ({
3244
3722
  const max = fieldSchema?.maximum;
3245
3723
  const step = isInteger ? 1 : fieldSchema?.multipleOf ?? "any";
3246
3724
  const tooltip = [label, description, hasError ? errors : null].filter(Boolean).join(" \u2014 ");
3247
- return /* @__PURE__ */ jsxs(
3248
- "div",
3725
+ const surface = (config ?? {}).surface ?? "form";
3726
+ const scale = scaleFor(surface);
3727
+ const input = /* @__PURE__ */ jsx(
3728
+ "input",
3249
3729
  {
3250
- className: `ss-jsonforms-field ss-jsonforms-number flex h-8 shrink-0 items-center gap-1.5 rounded-full border px-2.5 text-xs font-medium transition-colors focus-within:border-primary/40 ${hasError ? "border-destructive" : "border-border/70"} ${isDisabled ? "opacity-60" : ""}`,
3251
- title: tooltip || void 0,
3252
- children: [
3253
- label && /* @__PURE__ */ jsxs(
3254
- "label",
3255
- {
3256
- htmlFor: `number-${path2}`,
3257
- className: `whitespace-nowrap ${hasError ? "text-destructive" : "text-muted-foreground"}`,
3258
- children: [
3259
- label,
3260
- required && /* @__PURE__ */ jsx("span", { className: "ml-1 text-destructive", children: "*" })
3261
- ]
3262
- }
3263
- ),
3264
- /* @__PURE__ */ jsx(
3265
- "input",
3266
- {
3267
- id: `number-${path2}`,
3268
- type: "number",
3269
- value: data ?? "",
3270
- onChange: handleInputChange,
3271
- disabled: isDisabled,
3272
- min,
3273
- max,
3274
- step,
3275
- className: "w-14 border-0 bg-transparent p-0 text-xs font-medium tabular-nums text-foreground outline-none disabled:cursor-not-allowed"
3276
- }
3277
- )
3278
- ]
3730
+ id: `number-${path2}`,
3731
+ type: "number",
3732
+ value: data ?? "",
3733
+ onChange: handleInputChange,
3734
+ disabled: isDisabled,
3735
+ min,
3736
+ max,
3737
+ step,
3738
+ className: surface === "bar" ? "w-14 border-0 bg-transparent p-0 text-xs font-medium tabular-nums text-foreground outline-none disabled:cursor-not-allowed" : fieldNumberClass(scale, hasError)
3279
3739
  }
3280
3740
  );
3741
+ if (surface === "bar") {
3742
+ return /* @__PURE__ */ jsxs(
3743
+ "div",
3744
+ {
3745
+ className: `ss-jsonforms-field ss-jsonforms-number ${pillClass({
3746
+ hasError,
3747
+ disabled: isDisabled
3748
+ })}`,
3749
+ title: tooltip || void 0,
3750
+ children: [
3751
+ label && /* @__PURE__ */ jsxs(
3752
+ "label",
3753
+ {
3754
+ htmlFor: `number-${path2}`,
3755
+ className: `${pillLabelClass} whitespace-nowrap ${hasError ? "text-destructive" : "text-muted-foreground"}`,
3756
+ children: [
3757
+ label,
3758
+ required && /* @__PURE__ */ jsx("span", { className: "ml-1 text-destructive", children: "*" })
3759
+ ]
3760
+ }
3761
+ ),
3762
+ input
3763
+ ]
3764
+ }
3765
+ );
3766
+ }
3767
+ return /* @__PURE__ */ jsxs("div", { className: "ss-jsonforms-field ss-jsonforms-number", children: [
3768
+ /* @__PURE__ */ jsxs("div", { className: fieldRowClass, children: [
3769
+ /* @__PURE__ */ jsxs("label", { htmlFor: `number-${path2}`, className: fieldRowLabelClass(scale), children: [
3770
+ label,
3771
+ required && /* @__PURE__ */ jsx("span", { className: "ml-1 text-destructive", children: "*" })
3772
+ ] }),
3773
+ input
3774
+ ] }),
3775
+ hasError && /* @__PURE__ */ jsx("div", { className: fieldErrorClass, children: errors })
3776
+ ] });
3281
3777
  };
3282
3778
  var numberRendererTester = rankWith(
3283
3779
  40,
@@ -3293,6 +3789,14 @@ var numberRendererTester = rankWith(
3293
3789
  }
3294
3790
  );
3295
3791
  var NumberRendererControl = withJsonFormsControlProps(NumberRenderer);
3792
+ var SCALE = {
3793
+ comfortable: { lineHeight: 20, padding: 8 },
3794
+ dense: { lineHeight: 16, padding: 6 }
3795
+ };
3796
+ var rowsToPx = (rows, scale) => rows * scale.lineHeight + scale.padding * 2 + 2;
3797
+ var MAX_VIEWPORT_SHARE = 0.5;
3798
+ var DEFAULT_MIN_ROWS = 3;
3799
+ var DEFAULT_MAX_ROWS = 9;
3296
3800
  var TextareaRenderer = ({
3297
3801
  data,
3298
3802
  handleChange,
@@ -3304,28 +3808,49 @@ var TextareaRenderer = ({
3304
3808
  uischema,
3305
3809
  visible,
3306
3810
  enabled,
3307
- required
3811
+ required,
3812
+ config
3308
3813
  }) => {
3309
3814
  const textareaRef = useRef(null);
3815
+ const textareaOptions = schema["ui:textarea"] ?? {};
3816
+ const formConfig = config ?? {};
3817
+ const scaleName = scaleFor(formConfig.surface ?? "form");
3818
+ const scale = SCALE[scaleName];
3819
+ const minRows = textareaOptions.minRows ?? formConfig.minRows ?? DEFAULT_MIN_ROWS;
3820
+ const maxRows = Math.max(
3821
+ minRows,
3822
+ textareaOptions.maxRows ?? formConfig.maxRows ?? DEFAULT_MAX_ROWS
3823
+ );
3824
+ const minHeight = rowsToPx(minRows, scale);
3825
+ const maxHeight = rowsToPx(maxRows, scale);
3826
+ const [viewportCap, setViewportCap] = useState(
3827
+ () => typeof window === "undefined" ? Number.POSITIVE_INFINITY : Math.round(window.innerHeight * MAX_VIEWPORT_SHARE)
3828
+ );
3829
+ const ceiling = Math.max(minHeight, Math.min(maxHeight, viewportCap));
3310
3830
  const autoResize = useCallback(() => {
3311
- if (textareaRef.current) {
3312
- textareaRef.current.style.height = "auto";
3313
- textareaRef.current.style.height = `${Math.min(
3314
- Math.max(textareaRef.current.scrollHeight, 80),
3315
- // Minimum height of 80px
3316
- 240
3317
- // Maximum height of 240px
3318
- )}px`;
3319
- }
3320
- }, []);
3831
+ const el = textareaRef.current;
3832
+ if (!el) return;
3833
+ el.style.height = "auto";
3834
+ el.style.height = `${Math.min(
3835
+ Math.max(el.scrollHeight, minHeight),
3836
+ ceiling
3837
+ )}px`;
3838
+ }, [minHeight, ceiling]);
3321
3839
  useEffect(() => {
3322
3840
  autoResize();
3323
3841
  }, [data, autoResize]);
3842
+ useEffect(() => {
3843
+ const onResize = () => {
3844
+ setViewportCap(Math.round(window.innerHeight * MAX_VIEWPORT_SHARE));
3845
+ autoResize();
3846
+ };
3847
+ window.addEventListener("resize", onResize);
3848
+ return () => window.removeEventListener("resize", onResize);
3849
+ }, [autoResize]);
3324
3850
  const handleInputChange = useCallback(
3325
3851
  (event) => {
3326
- const newValue = event.target.value;
3327
- handleChange(path2, newValue);
3328
- setTimeout(autoResize, 0);
3852
+ handleChange(path2, event.target.value);
3853
+ autoResize();
3329
3854
  },
3330
3855
  [handleChange, path2, autoResize]
3331
3856
  );
@@ -3335,89 +3860,34 @@ var TextareaRenderer = ({
3335
3860
  const readOnly = uischema?.access === "Read";
3336
3861
  const isDisabled = !enabled || readOnly;
3337
3862
  const hasError = errors && errors.length > 0;
3338
- const textareaOptions = schema["ui:textarea"] ?? {};
3339
3863
  const placeholder = textareaOptions.placeholder || schema?.description || `Enter ${label?.toLowerCase() || "text"}...`;
3340
- const minRows = textareaOptions.minRows || 3;
3341
3864
  return /* @__PURE__ */ jsxs("div", { className: "ss-jsonforms-field ss-jsonforms-textarea", children: [
3342
- label && /* @__PURE__ */ jsxs(
3343
- "label",
3344
- {
3345
- style: {
3346
- display: "block",
3347
- color: hasError ? "#ef4444" : "#475569",
3348
- fontSize: "0.875rem",
3349
- fontWeight: 500,
3350
- marginBottom: "0.375rem"
3351
- },
3352
- children: [
3353
- label,
3354
- required && /* @__PURE__ */ jsx("span", { style: { color: "#ef4444", marginLeft: "0.25rem" }, children: "*" })
3355
- ]
3356
- }
3357
- ),
3358
- description && /* @__PURE__ */ jsx(
3359
- "div",
3360
- {
3361
- style: {
3362
- color: "#6b7280",
3363
- fontSize: "0.75rem",
3364
- marginBottom: "0.5rem"
3365
- },
3366
- children: description
3367
- }
3368
- ),
3865
+ label && /* @__PURE__ */ jsxs("label", { htmlFor: path2, className: fieldLabelClass(scaleName), children: [
3866
+ label,
3867
+ required && /* @__PURE__ */ jsx("span", { className: "ml-1 text-destructive", children: "*" })
3868
+ ] }),
3869
+ description && /* @__PURE__ */ jsx("div", { className: fieldHintClass, children: description }),
3369
3870
  /* @__PURE__ */ jsx(
3370
3871
  "textarea",
3371
3872
  {
3873
+ id: path2,
3372
3874
  ref: textareaRef,
3373
3875
  value: data || "",
3374
3876
  onChange: handleInputChange,
3375
3877
  placeholder,
3376
3878
  disabled: isDisabled,
3377
3879
  rows: minRows,
3880
+ className: `resize-y ${fieldControlClass(scaleName, !!hasError)}`,
3378
3881
  style: {
3379
- width: "100%",
3380
- minHeight: "80px",
3381
- maxHeight: "240px",
3382
- resize: "vertical",
3383
- padding: "0.75rem",
3384
- border: hasError ? "2px solid #ef4444" : "1px solid #d1d5db",
3385
- borderRadius: "6px",
3386
- fontSize: "16px",
3387
- lineHeight: "1.5",
3388
- fontFamily: "inherit",
3389
- backgroundColor: isDisabled ? "#f9fafb" : "#ffffff",
3390
- color: isDisabled ? "#9ca3af" : "#111827",
3391
- outline: "none",
3392
- transition: "border-color 0.2s ease-in-out, box-shadow 0.2s ease-in-out",
3393
- boxShadow: hasError ? "0 0 0 1px #ef4444" : "none",
3882
+ minHeight: `${minHeight}px`,
3883
+ maxHeight: `${ceiling}px`,
3884
+ // iOS zooms a focused control under 16px; the design's fields are
3885
+ // 14px, so opt out of the adjustment rather than the design.
3394
3886
  WebkitTextSizeAdjust: "100%"
3395
- },
3396
- onFocus: (e) => {
3397
- if (!hasError) {
3398
- e.target.style.borderColor = "#6366f1";
3399
- e.target.style.boxShadow = "0 0 0 1px #6366f1";
3400
- }
3401
- },
3402
- onBlur: (e) => {
3403
- if (!hasError) {
3404
- e.target.style.borderColor = "#d1d5db";
3405
- e.target.style.boxShadow = "none";
3406
- }
3407
3887
  }
3408
3888
  }
3409
3889
  ),
3410
- hasError && /* @__PURE__ */ jsx(
3411
- "div",
3412
- {
3413
- style: {
3414
- color: "#ef4444",
3415
- fontSize: "0.75rem",
3416
- marginTop: "0.25rem"
3417
- },
3418
- children: errors
3419
- }
3420
- )
3890
+ hasError && /* @__PURE__ */ jsx("div", { className: fieldErrorClass, children: errors })
3421
3891
  ] });
3422
3892
  };
3423
3893
  var textareaRendererTester = rankWith(
@@ -3469,10 +3939,25 @@ var renderers = [
3469
3939
  ];
3470
3940
 
3471
3941
  // src/chat-variables/VariablesForm.vm.ts
3942
+ var MAX_INLINE_VARIABLES = 3;
3943
+ var isModelSelector = (schema) => schema.title === "ModelId" || schema["x-model-selector"] === true;
3944
+ function rowLayout(controls) {
3945
+ const innerRow = {
3946
+ type: "HorizontalLayout",
3947
+ elements: controls,
3948
+ options: { gap: "12px", alignItems: "flex-start" }
3949
+ };
3950
+ const ui = {
3951
+ type: "VerticalLayout",
3952
+ elements: [innerRow]
3953
+ };
3954
+ return ui;
3955
+ }
3472
3956
  function buildSimpleSchemaAndUi(vars, threadVars, useDefaults) {
3473
3957
  const names = Object.keys(vars || {});
3474
3958
  const properties2 = {};
3475
3959
  const controls = [];
3960
+ const inlineOnly = [];
3476
3961
  const initialData = {};
3477
3962
  const $defs = {};
3478
3963
  for (const name of names) {
@@ -3492,19 +3977,24 @@ function buildSimpleSchemaAndUi(vars, threadVars, useDefaults) {
3492
3977
  control.enabled = false;
3493
3978
  }
3494
3979
  controls.push(control);
3980
+ if (isModelSelector(properties2[name])) inlineOnly.push(control);
3495
3981
  }
3496
3982
  const schema = { type: "object", properties: properties2 };
3497
3983
  if (Object.keys($defs).length > 0) schema.$defs = $defs;
3498
- const innerRow = {
3499
- type: "HorizontalLayout",
3500
- elements: controls,
3501
- options: { gap: "12px", alignItems: "flex-start" }
3502
- };
3503
- const ui = {
3504
- type: "VerticalLayout",
3505
- elements: [innerRow]
3984
+ if (controls.length <= MAX_INLINE_VARIABLES) {
3985
+ return {
3986
+ schema,
3987
+ inlineControls: controls,
3988
+ overflowControls: [],
3989
+ initialData
3990
+ };
3991
+ }
3992
+ return {
3993
+ schema,
3994
+ inlineControls: inlineOnly,
3995
+ overflowControls: controls.filter((c) => !inlineOnly.includes(c)),
3996
+ initialData
3506
3997
  };
3507
- return { schema, uiSchema: ui, initialData };
3508
3998
  }
3509
3999
  function useChatVariablesFormVm({
3510
4000
  workspace,
@@ -3519,26 +4009,26 @@ function useChatVariablesFormVm({
3519
4009
  const { mutate: updateVariableMutation } = useUpdateFlowRunVariable();
3520
4010
  const querySettled = !isLoading && (threadVars !== void 0 || isError);
3521
4011
  const shouldUseDefaults = isError || threadVars && Object.keys(threadVars).length === 0;
3522
- const built = React8.useMemo(() => {
4012
+ const built = React9.useMemo(() => {
3523
4013
  return buildSimpleSchemaAndUi(
3524
4014
  workspace.variables,
3525
4015
  threadVars,
3526
4016
  shouldUseDefaults ?? false
3527
4017
  );
3528
4018
  }, [workspace.variables, threadVars, shouldUseDefaults]);
3529
- const [data, setData] = React8.useState(null);
3530
- React8.useEffect(() => {
4019
+ const [data, setData] = React9.useState(null);
4020
+ React9.useEffect(() => {
3531
4021
  if (querySettled) {
3532
4022
  setData(built.initialData);
3533
4023
  setVariables(built.initialData);
3534
4024
  }
3535
4025
  }, [querySettled, built.initialData, setVariables]);
3536
- const ajv = React8.useMemo(() => createAjv({ useDefaults: false }), []);
3537
- const prevRef = React8.useRef(null);
3538
- React8.useEffect(() => {
4026
+ const ajv = React9.useMemo(() => createAjv({ useDefaults: false }), []);
4027
+ const prevRef = React9.useRef(null);
4028
+ React9.useEffect(() => {
3539
4029
  prevRef.current = data;
3540
4030
  }, [data]);
3541
- const onChange = React8.useCallback(
4031
+ const onChange = React9.useCallback(
3542
4032
  ({ data: next2 }) => {
3543
4033
  if (prevRef.current && !isDraftThreadId(threadId)) {
3544
4034
  const keys2 = Object.keys(workspace.variables || {});
@@ -3559,29 +4049,96 @@ function useChatVariablesFormVm({
3559
4049
  },
3560
4050
  [workspace.variables, setVariables, updateVariableMutation, threadId]
3561
4051
  );
3562
- const config = React8.useMemo(
4052
+ const barConfig = React9.useMemo(
3563
4053
  () => ({
3564
4054
  restrict: true,
3565
4055
  trim: false,
3566
4056
  showUnfocusedDescription: true,
3567
- hideRequiredAsterisk: true
4057
+ hideRequiredAsterisk: true,
4058
+ surface: "bar",
4059
+ minRows: 1,
4060
+ maxRows: 6
3568
4061
  }),
3569
4062
  []
3570
4063
  );
4064
+ const panelConfig = React9.useMemo(
4065
+ () => ({ ...barConfig, surface: "panel" }),
4066
+ [barConfig]
4067
+ );
4068
+ const inlineUiSchema = React9.useMemo(
4069
+ () => built.inlineControls.length ? rowLayout(built.inlineControls) : null,
4070
+ [built.inlineControls]
4071
+ );
4072
+ const overflowUiSchema = React9.useMemo(
4073
+ () => built.overflowControls.length ? rowLayout(built.overflowControls) : null,
4074
+ [built.overflowControls]
4075
+ );
3571
4076
  return {
3572
4077
  schema: built.schema,
3573
- uiSchema: built.uiSchema,
4078
+ inlineUiSchema,
4079
+ overflowUiSchema,
4080
+ overflowCount: built.overflowControls.length,
3574
4081
  data,
3575
4082
  renderers,
3576
4083
  cells,
3577
4084
  ajv,
3578
4085
  onChange,
3579
- config,
4086
+ barConfig,
4087
+ panelConfig,
3580
4088
  isLoading,
3581
4089
  isReady: querySettled,
3582
4090
  isHydrated: data !== null
3583
4091
  };
3584
4092
  }
4093
+ function VariablesOverflowPanel({ count, children: children2 }) {
4094
+ const triggerRef = useRef(null);
4095
+ const panelRef = useRef(null);
4096
+ const { isOpen, close, toggle, renderPopover } = useAnchoredPopover({
4097
+ trigger: triggerRef,
4098
+ popover: panelRef,
4099
+ width: POPOVER_WIDTH_PX,
4100
+ maxHeight: POPOVER_MAX_HEIGHT_PX,
4101
+ role: "dialog",
4102
+ label: "Workspace variables"
4103
+ });
4104
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
4105
+ /* @__PURE__ */ jsxs(
4106
+ "button",
4107
+ {
4108
+ ref: triggerRef,
4109
+ type: "button",
4110
+ onClick: toggle,
4111
+ "aria-expanded": isOpen,
4112
+ "aria-haspopup": "dialog",
4113
+ "aria-label": `Variables (${count})`,
4114
+ className: `flex h-8 shrink-0 items-center gap-1.5 rounded-full border px-2.5 text-xs font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary ${isOpen ? "border-primary/30 bg-primary/10 text-primary" : "border-border/70 bg-transparent text-muted-foreground hover:bg-secondary"}`,
4115
+ children: [
4116
+ /* @__PURE__ */ jsx(SlidersHorizontal, { className: "h-3.5 w-3.5 shrink-0" }),
4117
+ /* @__PURE__ */ jsx("span", { className: "inline max-sm:hidden", children: "Variables" }),
4118
+ /* @__PURE__ */ jsx("span", { className: "rounded-full bg-secondary px-1.5 text-[10px] leading-4 tabular-nums", children: count })
4119
+ ]
4120
+ }
4121
+ ),
4122
+ renderPopover(
4123
+ /* @__PURE__ */ jsxs(Fragment, { children: [
4124
+ /* @__PURE__ */ jsxs("div", { className: "flex shrink-0 items-center justify-between border-b border-border px-3 py-2", children: [
4125
+ /* @__PURE__ */ jsx("span", { className: popoverHeadingClass, children: "Variables" }),
4126
+ /* @__PURE__ */ jsx(
4127
+ "button",
4128
+ {
4129
+ type: "button",
4130
+ onClick: close,
4131
+ "aria-label": "Close variables",
4132
+ className: "inline-flex h-5 w-5 items-center justify-center rounded text-muted-foreground transition-colors hover:bg-secondary hover:text-foreground",
4133
+ children: /* @__PURE__ */ jsx(X, { className: "h-3.5 w-3.5" })
4134
+ }
4135
+ )
4136
+ ] }),
4137
+ /* @__PURE__ */ jsx("div", { className: "ss-variables-panel min-h-0 overflow-y-auto p-3", children: children2 })
4138
+ ] })
4139
+ )
4140
+ ] });
4141
+ }
3585
4142
  var ChatVariablesForm = forwardRef(({ workspace, threadId, setVariables }, ref) => {
3586
4143
  const vm = useChatVariablesFormVm({ workspace, threadId, setVariables });
3587
4144
  useImperativeHandle(ref, () => ({
@@ -3597,20 +4154,85 @@ var ChatVariablesForm = forwardRef(({ workspace, threadId, setVariables }, ref)
3597
4154
  if (!vm.isHydrated) {
3598
4155
  return /* @__PURE__ */ jsx("div", { className: "flex justify-center items-center w-full h-8", children: /* @__PURE__ */ jsx(Loader2, { className: "h-4 w-4 animate-spin text-primary" }) });
3599
4156
  }
3600
- return /* @__PURE__ */ jsx("div", { className: "w-full jsonforms-compact", children: /* @__PURE__ */ jsx(
3601
- JsonForms,
4157
+ const formProps = {
4158
+ schema: vm.schema,
4159
+ data: vm.data,
4160
+ renderers: vm.renderers,
4161
+ cells: vm.cells,
4162
+ ajv: vm.ajv,
4163
+ onChange: vm.onChange
4164
+ };
4165
+ return /* @__PURE__ */ jsxs("div", { className: "flex w-full min-w-0 items-center gap-1", children: [
4166
+ vm.inlineUiSchema && /* @__PURE__ */ jsx("div", { className: "min-w-0 jsonforms-compact", children: /* @__PURE__ */ jsx(
4167
+ JsonForms,
4168
+ {
4169
+ ...formProps,
4170
+ uischema: vm.inlineUiSchema,
4171
+ config: vm.barConfig
4172
+ }
4173
+ ) }),
4174
+ vm.overflowUiSchema && /* @__PURE__ */ jsx(VariablesOverflowPanel, { count: vm.overflowCount, children: /* @__PURE__ */ jsx("div", { className: "jsonforms-compact", children: /* @__PURE__ */ jsx(
4175
+ JsonForms,
4176
+ {
4177
+ ...formProps,
4178
+ uischema: vm.overflowUiSchema,
4179
+ config: vm.panelConfig
4180
+ }
4181
+ ) }) })
4182
+ ] });
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,
3602
4208
  {
3603
- schema: vm.schema,
3604
- uischema: vm.uiSchema,
3605
- data: vm.data,
3606
- renderers: vm.renderers,
3607
- cells: vm.cells,
3608
- ajv: vm.ajv,
3609
- onChange: vm.onChange,
3610
- config: vm.config
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
+ ]
3611
4233
  }
3612
- ) });
3613
- });
4234
+ );
4235
+ }
3614
4236
 
3615
4237
  // src/domains/workspaces/queryKeys.ts
3616
4238
  var workspaceKeys = {
@@ -3843,6 +4465,40 @@ function MessageComposer(_props = {}) {
3843
4465
  cancelReset();
3844
4466
  }
3845
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]);
3846
4502
  useEffect(() => {
3847
4503
  if (typeof window === "undefined") return;
3848
4504
  window.__ssDownloadFile = (id) => getFileBlobUrl(id);
@@ -3962,6 +4618,7 @@ function MessageComposer(_props = {}) {
3962
4618
  const latestText = editorRef.current?.getMarkdown() ?? newMessage;
3963
4619
  const sent = handleSendMessage(latestText, uploadedAttachments);
3964
4620
  if (!sent) return;
4621
+ stopDictation();
3965
4622
  handleClearAttachments();
3966
4623
  setEditorKey((k) => k + 1);
3967
4624
  };
@@ -4068,7 +4725,7 @@ function MessageComposer(_props = {}) {
4068
4725
  /* @__PURE__ */ jsx("div", { className: "max-h-[400px] w-full overflow-y-auto", children: /* @__PURE__ */ jsx(
4069
4726
  MarkdownEditor,
4070
4727
  {
4071
- ref: editorRef,
4728
+ ref: attachEditor,
4072
4729
  value: newMessage,
4073
4730
  onChange: (md) => setNewMessage(md),
4074
4731
  onKeyDown: handleComposerKeyDown,
@@ -4081,14 +4738,22 @@ function MessageComposer(_props = {}) {
4081
4738
  disabled,
4082
4739
  placeholder: "What would you like to do?",
4083
4740
  className: "md-editor--bare px-5 pb-3 pt-4 text-sm",
4084
- minHeight: 48
4741
+ minHeight: 24
4085
4742
  },
4086
4743
  `composer-md-${editorKey}`
4087
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] }),
4088
4753
  /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between gap-2 px-3 pb-3", children: [
4089
4754
  /* @__PURE__ */ jsxs("div", { className: "ss-composer__actions flex min-w-0 flex-1 items-center gap-1", children: [
4090
4755
  supportsFiles && /* @__PURE__ */ jsx(
4091
- IconButton,
4756
+ IconButton2,
4092
4757
  {
4093
4758
  type: "button",
4094
4759
  onClick: handlePickFilesClick,
@@ -4109,19 +4774,39 @@ function MessageComposer(_props = {}) {
4109
4774
  )
4110
4775
  ] }),
4111
4776
  /* @__PURE__ */ jsxs("div", { className: "flex shrink-0 items-center gap-1.5", children: [
4112
- /* @__PURE__ */ jsx(
4113
- IconButton,
4777
+ dictation.available ? /* @__PURE__ */ jsx(
4778
+ DictationButton,
4114
4779
  {
4115
- type: "button",
4116
- disabled: true,
4117
- "aria-label": "Dictate a message",
4118
- title: "Dictation is not available yet",
4119
- className: "h-8 w-8 cursor-not-allowed rounded-full text-muted-foreground",
4120
- 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
4121
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
+ )
4122
4807
  ),
4123
4808
  canStop ? /* @__PURE__ */ jsx(
4124
- IconButton,
4809
+ IconButton2,
4125
4810
  {
4126
4811
  onClick: handleStopRun,
4127
4812
  className: "chat-send h-8 w-8 rounded-full",
@@ -4135,7 +4820,7 @@ function MessageComposer(_props = {}) {
4135
4820
  )
4136
4821
  }
4137
4822
  ) : /* @__PURE__ */ jsx(
4138
- IconButton,
4823
+ IconButton2,
4139
4824
  {
4140
4825
  onClick: handleSendMessageAndClear,
4141
4826
  className: `chat-send h-8 w-8 rounded-full ${sendDisabled ? "cursor-not-allowed" : ""}`,
@@ -4164,7 +4849,7 @@ function MessageComposer(_props = {}) {
4164
4849
  },
4165
4850
  children: /* @__PURE__ */ jsxs("div", { className: "relative h-full w-full bg-background border shadow-lg", children: [
4166
4851
  /* @__PURE__ */ jsx(
4167
- IconButton,
4852
+ IconButton2,
4168
4853
  {
4169
4854
  type: "button",
4170
4855
  size: "small",
@@ -4182,7 +4867,7 @@ function MessageComposer(_props = {}) {
4182
4867
  /* @__PURE__ */ jsx("div", { className: "flex-1 p-4", children: /* @__PURE__ */ jsx(
4183
4868
  MarkdownEditor,
4184
4869
  {
4185
- ref: editorRef,
4870
+ ref: attachEditor,
4186
4871
  value: newMessage,
4187
4872
  onChange: (md) => setNewMessage(md),
4188
4873
  onKeyDown: handleComposerKeyDown,
@@ -4197,10 +4882,18 @@ function MessageComposer(_props = {}) {
4197
4882
  },
4198
4883
  `composer-md-${editorKey}`
4199
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
+ ),
4200
4893
  /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 px-3 py-2 border-t bg-background", children: [
4201
4894
  /* @__PURE__ */ jsx("div", { className: "flex-1" }),
4202
4895
  supportsFiles && /* @__PURE__ */ jsx(
4203
- IconButton,
4896
+ IconButton2,
4204
4897
  {
4205
4898
  type: "button",
4206
4899
  onClick: handlePickFilesClick,
@@ -4215,8 +4908,19 @@ function MessageComposer(_props = {}) {
4215
4908
  )
4216
4909
  }
4217
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
+ ),
4218
4922
  canStop ? /* @__PURE__ */ jsx(
4219
- IconButton,
4923
+ IconButton2,
4220
4924
  {
4221
4925
  onClick: handleStopRun,
4222
4926
  className: "chat-send h-9 w-9 rounded-full",
@@ -4230,7 +4934,7 @@ function MessageComposer(_props = {}) {
4230
4934
  )
4231
4935
  }
4232
4936
  ) : /* @__PURE__ */ jsx(
4233
- IconButton,
4937
+ IconButton2,
4234
4938
  {
4235
4939
  onClick: handleSendMessageAndClear,
4236
4940
  className: `chat-send h-9 w-9 rounded-full ${sendDisabled ? "cursor-not-allowed" : ""}`,
@@ -18944,6 +19648,79 @@ var getMessageErrorText = (error) => {
18944
19648
  }
18945
19649
  return STATUS_CODE_TEXT[error.code] ?? GENERIC_ERROR_TEXT;
18946
19650
  };
19651
+ var FileInfoSchema = z.object({
19652
+ id: z.string(),
19653
+ name: z.string()
19654
+ });
19655
+ z.object({
19656
+ workspaceId: z.string().optional(),
19657
+ threadId: z.string().optional()
19658
+ });
19659
+
19660
+ // src/domains/messages/schemas.ts
19661
+ var MessageResponseSourceSchema = z.object({
19662
+ index: z.number(),
19663
+ datasetItemId: z.string().optional(),
19664
+ containerItemId: z.string().nullish(),
19665
+ flowRunId: z.string().optional(),
19666
+ file: FileInfoSchema.nullish(),
19667
+ url: z.string().nullish(),
19668
+ sourceType: z.nativeEnum(MessageResponseSourceType),
19669
+ // WS3c citation grounding (additive; absent on older responses).
19670
+ uri: z.string().nullish(),
19671
+ citedText: z.string().nullish(),
19672
+ attribution: z.nativeEnum(MessageAttribution).nullish(),
19673
+ // Where the cited item lives at its source (SharePoint web link, HubSpot record page).
19674
+ // Distinct from `url` (the source *is* that URL): this rides File sources so the card
19675
+ // can offer "open at source" beside the download of our copy. Absent when unknown.
19676
+ webUrl: z.string().nullish()
19677
+ });
19678
+ var MessageResponseSchema = z.object({
19679
+ content: z.string(),
19680
+ messageId: z.string(),
19681
+ sources: z.array(MessageResponseSourceSchema).nullish(),
19682
+ isReplying: z.boolean().nullish().default(false),
19683
+ requestedJsonSchema: z.string().nullish()
19684
+ });
19685
+ var MessageErrorMessageSchema = z.object({
19686
+ code: z.number(),
19687
+ // Stable machine-readable category (e.g. "llm.rate_limit"). Accept both
19688
+ // spellings: app-api serialises camelCase, the ai-api origin field is
19689
+ // snake_case — the mapper coalesces them.
19690
+ errorCode: z.string().nullish(),
19691
+ error_code: z.string().nullish(),
19692
+ message: z.string().nullish(),
19693
+ data: z.string().nullish(),
19694
+ blockId: z.string().nullish()
19695
+ });
19696
+ var MessageSchema = z.object({
19697
+ id: z.string().nullish(),
19698
+ createdAt: DateFromApi,
19699
+ createdBy: z.string().nullish(),
19700
+ hasComments: z.boolean().default(false),
19701
+ createdByUserId: z.string().nullish(),
19702
+ messageThreadId: z.string().nullish(),
19703
+ errors: z.array(MessageErrorMessageSchema).nullish(),
19704
+ values: z.array(
19705
+ z.object({
19706
+ id: z.string(),
19707
+ name: z.string(),
19708
+ type: z.nativeEnum(MessageValueType),
19709
+ value: z.any(),
19710
+ // Can be string (for Output), array (for Input), or object (for Variables)
19711
+ channels: z.record(z.string(), z.number()),
19712
+ createdAt: DateFromApi,
19713
+ createdBy: z.string(),
19714
+ createdByUserId: z.string().nullish()
19715
+ })
19716
+ ).nullish(),
19717
+ optimistic: z.boolean().optional().default(false)
19718
+ });
19719
+ z.object({
19720
+ text: z.string().nullish(),
19721
+ image: FileInfoSchema.nullish()
19722
+ });
19723
+ z.array(MessageSchema);
18947
19724
 
18948
19725
  // src/domains/messages/statuses.ts
18949
19726
  var asRecord = (value) => {
@@ -19087,7 +19864,7 @@ var buttonVariants = cva(
19087
19864
  }
19088
19865
  }
19089
19866
  );
19090
- var Button = React8.forwardRef(
19867
+ var Button = React9.forwardRef(
19091
19868
  ({ className, variant, size, asChild = false, ...props }, ref) => {
19092
19869
  const Comp = asChild ? Slot : "button";
19093
19870
  return /* @__PURE__ */ jsx(
@@ -19638,6 +20415,7 @@ function ChatMessageSources({
19638
20415
  ] })
19639
20416
  ] });
19640
20417
  }
20418
+ var USER_FORM_CONFIG = { surface: "form", minRows: 6, maxRows: 20 };
19641
20419
  var REVEAL_ON_HOVER = "opacity-100 focus-visible:opacity-100 [@media(hover:hover)]:opacity-0 [@media(hover:hover)]:group-hover:opacity-100";
19642
20420
  var MessageBubble = (props) => {
19643
20421
  const {
@@ -19691,7 +20469,7 @@ var MessageBubble = (props) => {
19691
20469
  );
19692
20470
  })
19693
20471
  ] }),
19694
- showForm && /* @__PURE__ */ jsxs("div", { className: "mt-4 border-t border-border pt-4", children: [
20472
+ showForm && /* @__PURE__ */ jsxs("div", { className: "ss-chat-message__user-form mt-4 border-t border-border pt-4", children: [
19695
20473
  /* @__PURE__ */ jsx(
19696
20474
  JsonForms,
19697
20475
  {
@@ -19700,6 +20478,7 @@ var MessageBubble = (props) => {
19700
20478
  renderers,
19701
20479
  cells,
19702
20480
  readonly: userInput !== void 0,
20481
+ config: USER_FORM_CONFIG,
19703
20482
  onChange: ({ data, errors }) => {
19704
20483
  setResponseFormData(data);
19705
20484
  setResponseFormValid(!errors?.length);
@@ -19832,6 +20611,10 @@ function coerceSources(x) {
19832
20611
  if (!Array.isArray(x)) return [];
19833
20612
  return x.filter(isMessageResponseSource);
19834
20613
  }
20614
+ var ENVELOPE_KEYS = Object.keys(MessageResponseSchema.shape);
20615
+ function isResponseEnvelope(keys2) {
20616
+ return keys2.length > 0 && keys2.every((key) => ENVELOPE_KEYS.includes(key));
20617
+ }
19835
20618
  var MessageItem = ({
19836
20619
  message,
19837
20620
  isLive = false
@@ -19840,14 +20623,14 @@ var MessageItem = ({
19840
20623
  const { data: workspace } = useWorkspace(workspaceId);
19841
20624
  const chatbotName = getChatbotName(workspace?.name);
19842
20625
  const { addInputToMessageMutation } = useAddInputToMessage();
19843
- const onSubmitUserForm = (messageId) => (name, value) => {
20626
+ const onSubmitUserForm = (messageId, channels = {}) => (name, value) => {
19844
20627
  if (!threadId || !messageId) return;
19845
20628
  addInputToMessageMutation.mutate({
19846
20629
  threadId,
19847
20630
  messageId,
19848
20631
  name,
19849
20632
  value,
19850
- channels: {}
20633
+ channels
19851
20634
  });
19852
20635
  };
19853
20636
  const safeTime = (d) => {
@@ -19939,9 +20722,16 @@ var MessageItem = ({
19939
20722
  });
19940
20723
  } else if (name === "response" && typeof v.value === "object" && v.value !== null && !Array.isArray(v.value)) {
19941
20724
  const resp = v.value;
19942
- const isContentPart = "text" in resp || "image" in resp;
19943
- pushContent(groupContent, isContentPart ? resp : resp.content);
19944
- groupSources = coerceSources(resp.sources);
20725
+ const keys2 = Object.keys(resp);
20726
+ const isEnvelope = isResponseEnvelope(keys2);
20727
+ if ("sources" in resp && (isEnvelope || "text" in resp || "image" in resp)) {
20728
+ groupSources = coerceSources(resp.sources);
20729
+ }
20730
+ if (isEnvelope) {
20731
+ pushContent(groupContent, resp.content);
20732
+ } else if (keys2.length > 0) {
20733
+ pushContent(groupContent, resp);
20734
+ }
19945
20735
  } else {
19946
20736
  pushContent(groupContent, v.value);
19947
20737
  }
@@ -19971,7 +20761,7 @@ var MessageItem = ({
19971
20761
  userOutput: v.value && typeof v.value === "object" ? v.value : null,
19972
20762
  chatbotName,
19973
20763
  userInput: userInput?.value,
19974
- onSubmitUserForm: onSubmitUserForm(message.id ?? "")
20764
+ onSubmitUserForm: onSubmitUserForm(message.id ?? "", v.channels)
19975
20765
  },
19976
20766
  `user-${message.id ?? "msg"}-${keyCounter++}`
19977
20767
  )
@@ -20167,6 +20957,7 @@ function MessageList({
20167
20957
  ] }) });
20168
20958
  }
20169
20959
  if (safeMessages.length === 0 && !hadMessagesBefore) {
20960
+ const descriptionParagraphs = (activeWorkspace?.summary ?? "").split(/\n+/).map((paragraph2) => paragraph2.trim()).filter(Boolean);
20170
20961
  return (
20171
20962
  // A new thread opens the way the design does: the greeting and the
20172
20963
  // composer travel together as one narrower block centred in the canvas.
@@ -20179,6 +20970,14 @@ function MessageList({
20179
20970
  "data-ss-layer": "message-list",
20180
20971
  children: /* @__PURE__ */ jsxs("div", { className: "mx-auto mb-6 w-full max-w-2xl px-4 text-center", children: [
20181
20972
  /* @__PURE__ */ jsx("h2", { className: "text-2xl font-semibold text-foreground", children: "What\u2019s on the agenda today?" }),
20973
+ descriptionParagraphs.length > 0 && /* @__PURE__ */ jsx(
20974
+ "div",
20975
+ {
20976
+ className: "mx-auto mt-2 max-w-lg space-y-1.5 text-sm text-muted-foreground",
20977
+ "data-ss-layer": "workspace-description",
20978
+ children: descriptionParagraphs.map((paragraph2, index) => /* @__PURE__ */ jsx("p", { children: paragraph2 }, index))
20979
+ }
20980
+ ),
20182
20981
  activeWorkspace?.firstPrompt && /* @__PURE__ */ jsx("div", { className: "chat-prose mt-3 text-center", children: /* @__PURE__ */ jsx(MessageMarkdown, { value: activeWorkspace.firstPrompt }) })
20183
20982
  ] })
20184
20983
  }
@@ -20470,6 +21269,6 @@ function getUserPhotoUrl(userId) {
20470
21269
  return `${base2}/users/${userId}/photo`;
20471
21270
  }
20472
21271
 
20473
- 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 };
20474
21273
  //# sourceMappingURL=index.js.map
20475
21274
  //# sourceMappingURL=index.js.map