rift-ds 1.6.0 → 1.8.0

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.
Files changed (133) hide show
  1. package/README.md +34 -13
  2. package/bin/rift-ds.mjs +231 -44
  3. package/bin/templates/chat/.env.chat.example +21 -0
  4. package/bin/templates/chat/app/api/chat/feedback/route.ts +12 -0
  5. package/bin/templates/chat/app/api/chat/followups/route.ts +23 -0
  6. package/bin/templates/chat/app/api/chat/guardrails.ts +18 -0
  7. package/bin/templates/chat/app/api/chat/route.ts +31 -0
  8. package/bin/templates/chat/components/chat/ChatMount.tsx +52 -0
  9. package/bin/templates/chat/components/chat/models.ts +29 -0
  10. package/chat/AssistantTurn.d.ts +5 -0
  11. package/chat/AssistantTurn.js +82 -0
  12. package/chat/ChatAgentPane.css +56 -0
  13. package/chat/ChatAgentPane.d.ts +21 -0
  14. package/chat/ChatAgentPane.js +25 -0
  15. package/chat/ChatLauncher.d.ts +64 -0
  16. package/chat/ChatLauncher.js +347 -0
  17. package/chat/ChatProvider.d.ts +53 -0
  18. package/chat/ChatProvider.js +181 -0
  19. package/chat/ChatTabs.d.ts +13 -0
  20. package/chat/ChatTabs.js +24 -0
  21. package/chat/ChatThreads.d.ts +26 -0
  22. package/chat/ChatThreads.js +55 -0
  23. package/chat/ChatWidget.css +1072 -0
  24. package/chat/ChatWidget.d.ts +113 -0
  25. package/chat/ChatWidget.js +495 -0
  26. package/chat/DemoWelcome.d.ts +13 -0
  27. package/chat/DemoWelcome.js +131 -0
  28. package/chat/FollowupSuggestions.d.ts +23 -0
  29. package/chat/FollowupSuggestions.js +34 -0
  30. package/chat/ResponseActions.d.ts +18 -0
  31. package/chat/ResponseActions.js +79 -0
  32. package/chat/TurnContent.d.ts +9 -0
  33. package/chat/TurnContent.js +12 -0
  34. package/chat/context.d.ts +65 -0
  35. package/chat/context.js +27 -0
  36. package/chat/demo-agent-panes.d.ts +5 -0
  37. package/chat/demo-agent-panes.js +49 -0
  38. package/chat/demo-agent.d.ts +3 -0
  39. package/chat/demo-agent.js +28 -0
  40. package/chat/demo-card-components.d.ts +46 -0
  41. package/chat/demo-card-components.js +59 -0
  42. package/chat/demo-cards.d.ts +32 -0
  43. package/chat/demo-cards.js +59 -0
  44. package/chat/demo-fixtures.d.ts +20 -0
  45. package/chat/demo-fixtures.js +113 -0
  46. package/chat/demo-script.d.ts +6 -0
  47. package/chat/demo-script.js +215 -0
  48. package/chat/demo.css +46 -0
  49. package/chat/demo.d.ts +9 -0
  50. package/chat/demo.js +24 -0
  51. package/chat/followups.d.ts +41 -0
  52. package/chat/followups.js +55 -0
  53. package/chat/greeting.d.ts +11 -0
  54. package/chat/greeting.js +21 -0
  55. package/chat/index.d.ts +34 -0
  56. package/chat/index.js +80 -0
  57. package/chat/placement.d.ts +48 -0
  58. package/chat/placement.js +39 -0
  59. package/chat/queries.d.ts +9 -0
  60. package/chat/queries.js +22 -0
  61. package/chat/registry.d.ts +25 -0
  62. package/chat/registry.js +9 -0
  63. package/chat/registry.json.d.ts +69 -0
  64. package/chat/registry.json.js +11 -0
  65. package/chat/server.d.ts +217 -0
  66. package/chat/server.js +488 -0
  67. package/chat/sim.d.ts +71 -0
  68. package/chat/sim.js +125 -0
  69. package/chat/starters.d.ts +15 -0
  70. package/chat/starters.js +38 -0
  71. package/chat/suggestions.d.ts +22 -0
  72. package/chat/suggestions.js +9 -0
  73. package/chat/threads.d.ts +73 -0
  74. package/chat/threads.js +270 -0
  75. package/chat/transport.d.ts +49 -0
  76. package/chat/transport.js +105 -0
  77. package/chat/types.d.ts +210 -0
  78. package/chat/types.js +16 -0
  79. package/chat/upstash.d.ts +63 -0
  80. package/chat/upstash.js +147 -0
  81. package/chat/useChat.d.ts +64 -0
  82. package/chat/useChat.js +241 -0
  83. package/cli/templates/chat/app/api/chat/feedback/route.d.ts +3 -0
  84. package/cli/templates/chat/app/api/chat/followups/route.d.ts +3 -0
  85. package/cli/templates/chat/app/api/chat/guardrails.d.ts +10 -0
  86. package/cli/templates/chat/app/api/chat/route.d.ts +4 -0
  87. package/cli/templates/chat/components/chat/ChatMount.d.ts +1 -0
  88. package/cli/templates/chat/components/chat/models.d.ts +11 -0
  89. package/components/AgentRail/AgentRail.css +2 -2
  90. package/components/Attachment/Attachment.css +14 -0
  91. package/components/Attachment/AttachmentGroup.d.ts +2 -2
  92. package/components/Attachment/AttachmentGroup.js +7 -2
  93. package/components/Composer/Composer.css +88 -4
  94. package/components/Composer/Composer.d.ts +21 -0
  95. package/components/Composer/Composer.js +199 -14
  96. package/components/Mention/Mention.css +195 -0
  97. package/components/Mention/Mention.d.ts +68 -0
  98. package/components/Mention/Mention.js +145 -0
  99. package/components/Mention/mentions.d.ts +81 -0
  100. package/components/Mention/mentions.js +83 -0
  101. package/components/ShaderField/ShaderField.d.ts +6 -5
  102. package/components/ShaderField/useShaderField.d.ts +2 -1
  103. package/components/ShaderField/useShaderField.js +6 -6
  104. package/components/StatusDot/StatusDot.css +36 -13
  105. package/components/StatusDot/StatusDot.d.ts +16 -4
  106. package/components/StatusDot/StatusDot.js +25 -5
  107. package/components/ThreadPanel/ThreadPanel.css +20 -11
  108. package/components/ThreadPanel/ThreadPanel.d.ts +15 -3
  109. package/components/ThreadPanel/ThreadPanel.js +30 -6
  110. package/components/ThreadTabs/ThreadTabs.css +18 -8
  111. package/components/ThreadTabs/ThreadTabs.d.ts +8 -2
  112. package/components/ThreadTabs/ThreadTabs.js +24 -5
  113. package/components/registry.json +9 -1
  114. package/components/registry.json.d.ts +9 -1
  115. package/components/registry.json.js +1 -1
  116. package/fonts/material-symbols.css +2 -1
  117. package/index.d.ts +1 -0
  118. package/index.js +11 -0
  119. package/package.json +43 -2
  120. package/tokens/presets/coral.css +18 -2
  121. package/tokens/presets/forest.css +18 -2
  122. package/tokens/presets/gold.css +18 -2
  123. package/tokens/presets/mono.css +18 -2
  124. package/tokens/presets/pink.css +18 -2
  125. package/tokens/presets/terminal.css +18 -2
  126. package/tokens/presets/violet.css +18 -2
  127. package/tokens/presets/warm.css +18 -2
  128. package/tokens/presets/zest.css +18 -2
  129. package/tokens/registry.json +6 -0
  130. package/tokens/registry.json.d.ts +6 -0
  131. package/tokens/registry.json.js +1 -1
  132. package/tokens/tokens-dark.css +10 -0
  133. package/tokens/tokens-light.css +10 -0
@@ -0,0 +1,113 @@
1
+ import { ReactNode } from 'react';
2
+ import { AgentRailProfile, AgentRailTab } from '../components/AgentRail/AgentRail';
3
+ import { MentionSource } from '../components/Mention/Mention';
4
+ import { ChatThreadsProps } from './ChatThreads';
5
+ import { ChatModelOption, Starter } from './types';
6
+ /** A rail slot: the widget's own panel, or a render prop for a custom one. */
7
+ export type RailSlot = boolean | ((ctx: {
8
+ overlay: boolean;
9
+ close: () => void;
10
+ }) => ReactNode);
11
+ /** What the built-in agent pane shows. */
12
+ export interface ChatAgentOptions {
13
+ /** The agent's portrait block. Defaults to the widget's title with a "Connected" status. A profile with neither `avatar` nor `avatarSrc` wears a PixelAvatar seeded by the widget's title, the mark the header toggle wears. */
14
+ profile?: AgentRailProfile;
15
+ /** The panes, in display order. */
16
+ tabs: AgentRailTab[];
17
+ /** Open the pane at mount, where the panel is wide enough to seat it inline. Off by default: the header button opens it. */
18
+ defaultOpen?: boolean;
19
+ }
20
+ /** Props owned by ChatWidget itself. */
21
+ export interface ChatWidgetProps {
22
+ /** Show the expand toggle. A host whose stage is always a takeover hides it. */
23
+ fullscreenEnabled?: boolean;
24
+ /** Show the header's close button. A host that seats the chat as the page
25
+ itself rather than as a panel over one has nothing to close back to,
26
+ so it turns this off. */
27
+ closeEnabled?: boolean;
28
+ /** Show the header's Chat position menu (dock right, dock left, floating). Off by default: a host turns it on only when it implements the seats the menu offers, as ChatLauncher does. */
29
+ placementEnabled?: boolean;
30
+ /** Narrow insets for phone-width hosts. */
31
+ compact?: boolean;
32
+ /** The host is a phone viewport, where the soft keyboard will claim the
33
+ lower half of the screen the moment the composer is focused. The
34
+ welcome screen stacks instead of centring the composer: greeting in
35
+ the middle of the empty thread, starters directly above the composer,
36
+ composer pinned to the bottom from the first paint. */
37
+ phone?: boolean;
38
+ /** The header brand name. */
39
+ title?: string;
40
+ /** The composer's placeholder text. */
41
+ placeholder?: string;
42
+ /** Show the conversation starters on the welcome screen. */
43
+ showStarters?: boolean;
44
+ /** The header mark's image src; null (the default) shows the name alone. */
45
+ logo?: string | null;
46
+ /** The welcome screen's line under the greeting. */
47
+ tagline?: string;
48
+ /** The conversation starters: a few pre-written questions. Each must fit the chip budget (SUGGESTION_MAX_CHARS); one that does not is dropped rather than clipped. */
49
+ starters?: Starter[];
50
+ /** Keys the starters' staged reveal: a new key replays the generation beat, the same key shows the set standing. A host whose starters follow the page passes the pathname. */
51
+ startersKey?: string;
52
+ /** Replaces the composer's leading actions (the model picker). */
53
+ composerActions?: ReactNode;
54
+ /** The models the picker offers. The pick rides on each send as a wire value; the server resolves it. Omit for no picker. */
55
+ models?: ChatModelOption[];
56
+ /** The picker's selection before the server has reported its default. Defaults to the first model. */
57
+ defaultModel?: string;
58
+ /** The one line a locked model shows in place of its description. */
59
+ lockedModelDescription?: string;
60
+ /** Let the visitor attach files: an attach button, paste, drop anywhere
61
+ on the chat, and long pastes folded into a tile. Files are read in the
62
+ browser for previews; what reaches the transport is names and kinds. */
63
+ fileAttachments?: boolean;
64
+ /** Most files the visitor may queue on one message. */
65
+ maxAttachments?: number;
66
+ /** Largest file the visitor may queue, in bytes. */
67
+ maxAttachmentBytes?: number;
68
+ /** What the visitor can reference while typing: one source per trigger
69
+ character, drawn as tags in the composer and in the sent bubble. */
70
+ mentions?: MentionSource[];
71
+ /** A session-history rail. `true` seats the built-in ChatThreads over the
72
+ provider's thread lifecycle; a render prop seats your own. Wide hosts
73
+ seat it as an inline left rail, narrow ones summon it from a header
74
+ hamburger as a slide-over sheet behind a scrim. */
75
+ threads?: RailSlot;
76
+ /** Props for the built-in threads rail when `threads` is `true`. */
77
+ threadsProps?: Omit<ChatThreadsProps, 'overlay' | 'close'>;
78
+ /** An open-sessions tab strip as its own row under the header. `true` seats the built-in ChatTabs; a node seats your own. */
79
+ tabs?: boolean | ReactNode;
80
+ /** A companion rail on the trailing edge, the mirror of `threads`: a render prop for your own pane, seated inline on a wide host and raised as a bottom sheet from a header button on a narrow one. For the built-in pane pass `agent` instead. */
81
+ aside?: (ctx: {
82
+ overlay: boolean;
83
+ close: () => void;
84
+ }) => ReactNode;
85
+ /** The built-in agent pane: who the agent is and what it has been doing. Seated like `aside`. */
86
+ agent?: ChatAgentOptions;
87
+ /** The composer's context note, naming what the chat is looking at. Omit for none. */
88
+ contextLabel?: string | null;
89
+ /** A widget slot on the welcome screen, under the composer and its
90
+ starters. It takes any content and keeps its natural height, so the
91
+ composer rides up by half of it instead of holding dead centre: the
92
+ empty chat reads as a home page rather than a blank prompt. It leaves
93
+ with the first message, like the rest of the welcome dressing. */
94
+ widgets?: ReactNode;
95
+ /** How the widget slot takes its room. `fit` keeps the welcome screen to
96
+ one viewport: the widgets hold their height and the composer rides up
97
+ to make room. `scroll` turns the welcome screen into a page: the
98
+ greeting and composer take the opening of the viewport, the widgets
99
+ run on below the fold, and the whole welcome scrolls as one. */
100
+ widgetsLayout?: 'fit' | 'scroll';
101
+ /** How wide the widget slot runs. `column` holds it to the composer's
102
+ bounding box; `wide` lets it run past them, for a dashboard. */
103
+ widgetsWidth?: 'column' | 'wide';
104
+ /** The quiet line pinned to the widget's floor: what the chat is and where its policy lives. */
105
+ disclaimer?: ReactNode;
106
+ }
107
+ /**
108
+ * The composed chat: header, thread, composer, starters, disclaimer, and the
109
+ * optional rails, filling whatever container hosts it (ChatLauncher's panel,
110
+ * a stage card, a whole page). It reads all of its state from ChatProvider,
111
+ * so the conversation is the same object wherever it shows.
112
+ */
113
+ export declare function ChatWidget({ fullscreenEnabled, closeEnabled, placementEnabled, compact, phone, title, placeholder, showStarters, logo, tagline, starters: startersInput, startersKey, composerActions, models, defaultModel, lockedModelDescription, fileAttachments, maxAttachments, maxAttachmentBytes, mentions, threads, threadsProps, tabs, aside, agent, contextLabel, widgets, widgetsLayout, widgetsWidth, disclaimer, }: ChatWidgetProps): import("react").JSX.Element;
@@ -0,0 +1,495 @@
1
+ "use client";
2
+ import { jsx, jsxs, Fragment } from "react/jsx-runtime";
3
+ import { useSyncExternalStore, useRef, useEffect, useState, useId } from "react";
4
+ import { AttachmentDropZone } from "../components/Attachment/AttachmentDropZone.js";
5
+ import { AttachmentGroup } from "../components/Attachment/AttachmentGroup.js";
6
+ import { useAttachments } from "../components/Attachment/useAttachments.js";
7
+ import { ChatHeader } from "../components/ChatHeader/ChatHeader.js";
8
+ import { ChatMessage } from "../components/ChatMessage/ChatMessage.js";
9
+ import { ChatThread } from "../components/ChatThread/ChatThread.js";
10
+ import { CircularButton } from "../components/CircularButton/CircularButton.js";
11
+ import { PixelAvatar } from "../components/PixelAvatar/PixelAvatar.js";
12
+ import { Composer } from "../components/Composer/Composer.js";
13
+ import { DropdownMenu } from "../components/DropdownMenu/DropdownMenu.js";
14
+ import { MentionText } from "../components/Mention/Mention.js";
15
+ import { ModelPicker } from "../components/ModelPicker/ModelPicker.js";
16
+ import { PromptSuggestions } from "../components/PromptSuggestions/PromptSuggestions.js";
17
+ import { MOTION_SUGGESTIONS_THINK_MS, MOTION_SCROLL_SETTLE_MS } from "../tokens/motion.js";
18
+ import { AssistantTurn } from "./AssistantTurn.js";
19
+ import { ChatAgentPane } from "./ChatAgentPane.js";
20
+ import { ChatTabs } from "./ChatTabs.js";
21
+ import { ChatThreads } from "./ChatThreads.js";
22
+ import { useChatWidget } from "./context.js";
23
+ import { subscribeClock, readGreeting, serverGreeting } from "./greeting.js";
24
+ import { CHAT_PLACEMENTS, chatPlacement } from "./placement.js";
25
+ import { THREADS_RAIL_MIN_WIDTH, ASIDE_RAIL_MIN_WIDTH } from "./queries.js";
26
+ import { fitsChip } from "./suggestions.js";
27
+ import { TurnCard } from "./TurnContent.js";
28
+ import "./ChatWidget.css";
29
+ const DEFAULT_MAX_ATTACHMENTS = 10;
30
+ const DEFAULT_MAX_ATTACHMENT_BYTES = 50 * 1024 * 1024;
31
+ const LONG_PASTE_CHARS = 1200;
32
+ const formatBytes = (bytes) => bytes >= 1024 * 1024 * 1024 ? `${Math.round(bytes / (1024 * 1024 * 1024))} GB` : `${Math.round(bytes / (1024 * 1024))} MB`;
33
+ const withAgentMark = (profile, title) => profile.avatar || profile.avatarSrc ? profile : { ...profile, avatar: /* @__PURE__ */ jsx(PixelAvatar, { name: title, size: "lg", label: title }) };
34
+ function ChatWidget({
35
+ fullscreenEnabled = true,
36
+ closeEnabled = true,
37
+ placementEnabled = false,
38
+ compact = false,
39
+ phone = false,
40
+ title = "Assistant",
41
+ placeholder = "Ask anything",
42
+ showStarters = true,
43
+ logo = null,
44
+ tagline = "How can I help you today?",
45
+ starters: startersInput,
46
+ startersKey = "",
47
+ composerActions,
48
+ models,
49
+ defaultModel,
50
+ lockedModelDescription = "Paused for today to stay in budget.",
51
+ fileAttachments = false,
52
+ maxAttachments = DEFAULT_MAX_ATTACHMENTS,
53
+ maxAttachmentBytes = DEFAULT_MAX_ATTACHMENT_BYTES,
54
+ mentions,
55
+ threads,
56
+ threadsProps,
57
+ tabs,
58
+ aside,
59
+ agent,
60
+ contextLabel,
61
+ widgets,
62
+ widgetsLayout = "fit",
63
+ widgetsWidth = "column",
64
+ disclaimer = "Answers may be wrong."
65
+ }) {
66
+ const {
67
+ turns,
68
+ live,
69
+ streaming,
70
+ modelPolicy,
71
+ chosenModel,
72
+ setChosenModel,
73
+ send,
74
+ stop,
75
+ reset,
76
+ open,
77
+ setOpen,
78
+ view,
79
+ setView,
80
+ placement,
81
+ setPlacement,
82
+ draft,
83
+ setDraft,
84
+ starterSeed,
85
+ starterRevealKey,
86
+ setStarterRevealKey
87
+ } = useChatWidget();
88
+ const lockedValues = modelPolicy?.locked ?? [];
89
+ const pick = chosenModel && !lockedValues.includes(chosenModel) ? chosenModel : null;
90
+ const pickerValue = pick ?? modelPolicy?.default ?? defaultModel ?? models?.[0]?.value;
91
+ const pickerModels = (models ?? []).map((option) => ({
92
+ value: option.value,
93
+ label: option.label,
94
+ description: lockedValues.includes(option.value) ? lockedModelDescription : option.description,
95
+ disabled: lockedValues.includes(option.value)
96
+ }));
97
+ const greeting = useSyncExternalStore(subscribeClock, readGreeting, serverGreeting);
98
+ const starters = (startersInput ?? []).filter((starter) => fitsChip(starter.label));
99
+ const composerRef = useRef(null);
100
+ const focusComposer = () => composerRef.current?.focus({ preventScroll: true });
101
+ useEffect(() => {
102
+ if (open) focusComposer();
103
+ }, [open]);
104
+ const queue = useAttachments({ maxCount: maxAttachments, maxSize: maxAttachmentBytes });
105
+ const attachHint = `Up to ${formatBytes(maxAttachmentBytes)} each · ${maxAttachments} files max`;
106
+ const handleSubmit = (value) => {
107
+ if (fileAttachments && queue.items.length > 0) {
108
+ const ready = queue.items.filter((item) => item.status === "ready");
109
+ if (send(value, { attachments: ready })) {
110
+ queue.take();
111
+ setDraft("");
112
+ }
113
+ } else if (send(value)) setDraft("");
114
+ focusComposer();
115
+ };
116
+ const isFull = view === "full";
117
+ const isEmpty = turns.length === 0 && !live;
118
+ const [stagedKey, setStagedKey] = useState(null);
119
+ const startersThinking = open && isEmpty && starterRevealKey !== startersKey;
120
+ const startersEntrance = startersThinking || stagedKey === startersKey;
121
+ useEffect(() => {
122
+ if (!(open && isEmpty && starterRevealKey !== startersKey)) return;
123
+ const timer = setTimeout(() => {
124
+ setStagedKey(startersKey);
125
+ setStarterRevealKey(startersKey);
126
+ }, MOTION_SUGGESTIONS_THINK_MS);
127
+ return () => clearTimeout(timer);
128
+ }, [open, isEmpty, starterRevealKey, setStarterRevealKey, startersKey]);
129
+ const renderThreads = threads === true ? (ctx) => /* @__PURE__ */ jsx(ChatThreads, { ...threadsProps, logoText: threadsProps?.logoText ?? title, ...ctx }) : threads || void 0;
130
+ const renderAside = aside ?? (agent ? (ctx) => /* @__PURE__ */ jsx(
131
+ ChatAgentPane,
132
+ {
133
+ profile: withAgentMark(agent.profile ?? { name: title, status: "Connected" }, title),
134
+ tabs: agent.tabs,
135
+ overlay: ctx.overlay
136
+ }
137
+ ) : void 0);
138
+ const tabsNode = tabs === true ? /* @__PURE__ */ jsx(ChatTabs, {}) : tabs || null;
139
+ const hasRails = Boolean(renderThreads || renderAside);
140
+ const [threadsOpen, setThreadsOpen] = useState(false);
141
+ const [asideOpen, setAsideOpen] = useState(Boolean(agent?.defaultOpen));
142
+ const railsHostRef = useRef(null);
143
+ const [threadsWide, setThreadsWide] = useState(false);
144
+ const [asideWide, setAsideWide] = useState(false);
145
+ const wasAsideWideRef = useRef(false);
146
+ useEffect(() => {
147
+ if (!hasRails) return;
148
+ const host = railsHostRef.current;
149
+ if (!host) return;
150
+ const measure = () => {
151
+ const width = host.clientWidth;
152
+ const wide = width >= THREADS_RAIL_MIN_WIDTH;
153
+ const asideRoom = width >= ASIDE_RAIL_MIN_WIDTH;
154
+ setThreadsWide(wide);
155
+ setAsideWide(asideRoom);
156
+ if (wide) setThreadsOpen(false);
157
+ if (wasAsideWideRef.current && !asideRoom) setAsideOpen(false);
158
+ wasAsideWideRef.current = asideRoom;
159
+ };
160
+ measure();
161
+ const observer = new ResizeObserver(measure);
162
+ observer.observe(host);
163
+ return () => observer.disconnect();
164
+ }, [hasRails]);
165
+ const closeThreads = () => setThreadsOpen(false);
166
+ const asideId = useId();
167
+ const asideShown = Boolean(renderAside) && asideOpen;
168
+ const closeAside = () => setAsideOpen(false);
169
+ const toggleAside = () => setAsideOpen((current) => !current);
170
+ const startersNode = isEmpty && showStarters && starters.length > 0 && /* @__PURE__ */ jsx("div", { className: "ds-chat-widget__starters", children: /* @__PURE__ */ jsx(
171
+ PromptSuggestions,
172
+ {
173
+ layout: "stack",
174
+ ariaLabel: "Conversation starters",
175
+ pending: startersThinking,
176
+ entrance: startersEntrance,
177
+ suggestions: starters,
178
+ onValueChange: (id) => {
179
+ const starter = starters.find((s) => s.id === id);
180
+ if (starter && send(starter.label)) focusComposer();
181
+ }
182
+ }
183
+ ) });
184
+ const widgetsNode = isEmpty && widgets && /* @__PURE__ */ jsx("div", { className: "ds-chat-widget__widgets", children: widgets });
185
+ const headerActions = /* @__PURE__ */ jsxs(Fragment, { children: [
186
+ !(renderThreads && threadsWide) && /* @__PURE__ */ jsx(
187
+ CircularButton,
188
+ {
189
+ icon: "edit_square",
190
+ variant: "tertiary",
191
+ ariaLabel: "New chat",
192
+ tooltipPosition: "bottom",
193
+ onClick: () => {
194
+ reset();
195
+ setDraft("");
196
+ focusComposer();
197
+ }
198
+ }
199
+ ),
200
+ renderAside && /* @__PURE__ */ jsx(
201
+ CircularButton,
202
+ {
203
+ icon: /* @__PURE__ */ jsx(PixelAvatar, { name: title, size: "sm" }),
204
+ variant: "tertiary",
205
+ className: asideShown ? "ds-chat-widget__agent-toggle--on" : void 0,
206
+ ariaLabel: asideShown ? "Hide the agent panel" : "Show the agent panel",
207
+ tooltipPosition: "bottom",
208
+ "aria-expanded": asideShown,
209
+ "aria-controls": asideShown ? asideId : void 0,
210
+ onClick: toggleAside
211
+ }
212
+ ),
213
+ placementEnabled && /* @__PURE__ */ jsx(
214
+ "span",
215
+ {
216
+ className: "ds-chat-widget__placement-menu",
217
+ onKeyDown: (event) => {
218
+ if (event.key === "Escape" && event.currentTarget.querySelector('[aria-expanded="true"]')) {
219
+ event.stopPropagation();
220
+ }
221
+ },
222
+ children: /* @__PURE__ */ jsx(
223
+ DropdownMenu,
224
+ {
225
+ align: "end",
226
+ trigger: /* @__PURE__ */ jsx(
227
+ CircularButton,
228
+ {
229
+ icon: chatPlacement(placement).icon,
230
+ variant: "tertiary",
231
+ ariaLabel: "Chat position",
232
+ tooltipPosition: "bottom",
233
+ disabled: isFull
234
+ }
235
+ ),
236
+ items: [
237
+ {
238
+ type: "group",
239
+ label: "Chat position",
240
+ items: CHAT_PLACEMENTS.map(({ id, label, icon }) => ({
241
+ label,
242
+ icon,
243
+ selected: placement === id,
244
+ onClick: () => setPlacement(id)
245
+ }))
246
+ }
247
+ ]
248
+ }
249
+ )
250
+ }
251
+ ),
252
+ fullscreenEnabled && /* @__PURE__ */ jsx(
253
+ CircularButton,
254
+ {
255
+ icon: isFull ? "close_fullscreen" : "open_in_full",
256
+ variant: "tertiary",
257
+ ariaLabel: isFull ? "Exit full screen" : "Enter full screen",
258
+ tooltipPosition: "bottom",
259
+ onClick: () => setView(isFull ? "panel" : "full")
260
+ }
261
+ ),
262
+ closeEnabled && /* @__PURE__ */ jsx(
263
+ CircularButton,
264
+ {
265
+ icon: "close",
266
+ variant: "tertiary",
267
+ ariaLabel: "Close chat",
268
+ tooltipPosition: "bottom",
269
+ onClick: () => setOpen(false)
270
+ }
271
+ )
272
+ ] });
273
+ const ChatRoot = fileAttachments ? AttachmentDropZone : "div";
274
+ const dropProps = fileAttachments ? { onFilesSelected: queue.add, hint: attachHint } : {};
275
+ const [rootScrolling, setRootScrolling] = useState(false);
276
+ const rootSettleTimer = useRef(void 0);
277
+ const handleRootScroll = () => {
278
+ if (widgetsLayout !== "scroll") return;
279
+ setRootScrolling(true);
280
+ window.clearTimeout(rootSettleTimer.current);
281
+ rootSettleTimer.current = window.setTimeout(() => setRootScrolling(false), MOTION_SCROLL_SETTLE_MS);
282
+ };
283
+ useEffect(() => () => window.clearTimeout(rootSettleTimer.current), []);
284
+ const chatColumn = /* @__PURE__ */ jsxs(
285
+ ChatRoot,
286
+ {
287
+ ...dropProps,
288
+ className: `ds-chat-widget ${renderThreads ? "ds-chat-widget--with-threads" : ""}`,
289
+ "data-compact": compact || void 0,
290
+ "data-phone": phone || void 0,
291
+ "data-widgets": widgetsNode ? widgetsLayout : void 0,
292
+ "data-widgets-wide": widgetsNode && widgetsWidth === "wide" ? "" : void 0,
293
+ "data-scrolling": widgetsNode && widgetsLayout === "scroll" && rootScrolling || void 0,
294
+ onScroll: handleRootScroll,
295
+ "data-lifted-actions": asideShown && asideWide || void 0,
296
+ onKeyDown: (e) => {
297
+ if (e.key === "Escape" && (threadsOpen || asideShown && !asideWide)) {
298
+ e.stopPropagation();
299
+ if (threadsOpen) closeThreads();
300
+ else closeAside();
301
+ return;
302
+ }
303
+ if (e.key === "Escape" && isFull) {
304
+ e.stopPropagation();
305
+ setView("panel");
306
+ }
307
+ },
308
+ onClick: (e) => {
309
+ if (!isEmpty) return;
310
+ const target = e.target;
311
+ if (target.closest("button, a, input, textarea, [role='button']")) return;
312
+ if (window.getSelection()?.toString()) return;
313
+ focusComposer();
314
+ },
315
+ children: [
316
+ /* @__PURE__ */ jsxs("div", { className: "ds-chat-widget__top", children: [
317
+ /* @__PURE__ */ jsx(
318
+ ChatHeader,
319
+ {
320
+ title: /* @__PURE__ */ jsxs("span", { className: "ds-chat-widget__brand", children: [
321
+ renderThreads && !threadsWide && /* @__PURE__ */ jsx(
322
+ CircularButton,
323
+ {
324
+ icon: "menu",
325
+ variant: "tertiary",
326
+ ariaLabel: "Open the chat history",
327
+ tooltipPosition: "bottom",
328
+ onClick: () => setThreadsOpen(true)
329
+ }
330
+ ),
331
+ logo && /* @__PURE__ */ jsx("img", { src: logo, alt: "", width: 20, height: 20 }),
332
+ /* @__PURE__ */ jsx("span", { className: "ds-chat-widget__brand-name", children: title })
333
+ ] }),
334
+ actions: (
335
+ /* With the agent rail seated inline the cluster moves to the
336
+ widget's own right edge (see headerActions). */
337
+ asideShown && asideWide ? void 0 : headerActions
338
+ )
339
+ }
340
+ ),
341
+ tabsNode && /* @__PURE__ */ jsx("div", { className: "ds-chat-widget__tabs-row", children: tabsNode }),
342
+ /* @__PURE__ */ jsxs("div", { className: "ds-chat-widget__body", children: [
343
+ /* @__PURE__ */ jsx(ChatThread, { className: "ds-chat-widget__thread", children: [
344
+ ...turns.map(
345
+ (turn) => turn.bare ? (
346
+ /* Conversation furniture (a day marker): the injected
347
+ content stands alone in the thread, no message chrome. */
348
+ /* @__PURE__ */ jsx("div", { children: turn.content }, turn.id)
349
+ ) : turn.role === "user" ? (
350
+ /* Files ride above the bubble on the sender's side; a turn
351
+ of files alone draws no bubble. */
352
+ /* @__PURE__ */ jsx(
353
+ ChatMessage,
354
+ {
355
+ role: "user",
356
+ attachments: turn.attachments ? /* @__PURE__ */ jsx(AttachmentGroup, { align: "end", items: turn.attachments }) : /* @__PURE__ */ jsxs(Fragment, { children: [
357
+ turn.content,
358
+ turn.card && /* @__PURE__ */ jsx(TurnCard, { card: turn.card })
359
+ ] }),
360
+ children: mentions && turn.text ? /* @__PURE__ */ jsx(MentionText, { text: turn.text, sources: mentions }) : turn.text
361
+ },
362
+ turn.id
363
+ )
364
+ ) : /* @__PURE__ */ jsx(AssistantTurn, { turn }, turn.id)
365
+ ),
366
+ ...live ? [/* @__PURE__ */ jsx(AssistantTurn, { live }, live.id)] : []
367
+ ] }),
368
+ isEmpty && /* @__PURE__ */ jsx("div", { className: "ds-chat-widget__welcome", children: /* @__PURE__ */ jsxs("div", { className: "ds-chat-widget__greeting", children: [
369
+ /* @__PURE__ */ jsx("p", { className: "ds-chat-widget__hello", children: greeting }),
370
+ /* @__PURE__ */ jsx("p", { className: "ds-chat-widget__ask", children: tagline })
371
+ ] }) })
372
+ ] })
373
+ ] }),
374
+ phone && startersNode,
375
+ phone && widgetsLayout === "fit" && widgetsNode,
376
+ /* @__PURE__ */ jsx("footer", { className: "ds-chat-widget__footer", children: /* @__PURE__ */ jsx("div", { className: "ds-chat-widget__composer", children: /* @__PURE__ */ jsx(
377
+ Composer,
378
+ {
379
+ ref: composerRef,
380
+ placeholder,
381
+ sendLabel: "Send",
382
+ aiGlow: true,
383
+ value: draft,
384
+ onValueChange: setDraft,
385
+ onSubmit: handleSubmit,
386
+ streaming,
387
+ onStop: stop,
388
+ context: contextLabel ? /* @__PURE__ */ jsxs(Fragment, { children: [
389
+ "Looking at “",
390
+ contextLabel,
391
+ "”"
392
+ ] }) : void 0,
393
+ contextIcon: "description",
394
+ contextPlacement: "above",
395
+ mentions,
396
+ ...fileAttachments ? {
397
+ files: queue.items,
398
+ onFilesSelected: queue.add,
399
+ onFileRemove: queue.remove,
400
+ pasteThreshold: LONG_PASTE_CHARS,
401
+ onPasteAsAttachment: (text) => queue.addText(text)
402
+ } : {},
403
+ actions: (
404
+ /* A host can slot its own leading actions. The default is the
405
+ model picker over the host's model list: the pick rides on
406
+ the next send, the server clamps it to the day's budget, and
407
+ the selection follows the server's default until the visitor
408
+ chooses. */
409
+ composerActions ?? (pickerModels.length > 0 ? /* @__PURE__ */ jsx(
410
+ ModelPicker,
411
+ {
412
+ models: pickerModels,
413
+ value: pickerValue,
414
+ onValueChange: setChosenModel,
415
+ placement: "top",
416
+ "aria-label": "Choose a model"
417
+ }
418
+ ) : void 0)
419
+ )
420
+ }
421
+ ) }) }),
422
+ /* @__PURE__ */ jsxs(
423
+ "div",
424
+ {
425
+ className: `ds-chat-widget__bottom ${isEmpty ? "ds-chat-widget__bottom--welcome" : ""} ${widgetsNode ? "ds-chat-widget__bottom--widgets" : ""}`,
426
+ children: [
427
+ !phone && startersNode,
428
+ (!phone || widgetsLayout === "scroll") && widgetsNode,
429
+ disclaimer && /* @__PURE__ */ jsx("div", { className: "ds-chat-widget__disclaimer-row", children: /* @__PURE__ */ jsx("p", { className: "ds-chat-widget__disclaimer", children: disclaimer }) })
430
+ ]
431
+ }
432
+ )
433
+ ]
434
+ }
435
+ );
436
+ if (!hasRails) return chatColumn;
437
+ return /* @__PURE__ */ jsxs("div", { ref: railsHostRef, className: "ds-chat-widget__rails", children: [
438
+ renderThreads && threadsWide && /* @__PURE__ */ jsx("div", { className: "ds-chat-widget__threads-rail", children: renderThreads({ overlay: false, close: closeThreads }) }),
439
+ chatColumn,
440
+ asideShown && asideWide && /* @__PURE__ */ jsx("div", { className: "ds-chat-widget__rail-actions", children: headerActions }),
441
+ renderAside && asideWide && /* @__PURE__ */ jsx(
442
+ "div",
443
+ {
444
+ className: `ds-chat-widget__aside-rail ${asideOpen ? "ds-chat-widget__aside-rail--open" : ""}`,
445
+ id: asideId,
446
+ inert: !asideOpen || void 0,
447
+ children: /* @__PURE__ */ jsx("div", { className: "ds-chat-widget__aside-rail-inner", children: renderAside({ overlay: false, close: closeAside }) })
448
+ }
449
+ ),
450
+ renderThreads && !threadsWide && /* @__PURE__ */ jsxs(
451
+ "div",
452
+ {
453
+ className: `ds-chat-widget__threads-overlay ${threadsOpen ? "ds-chat-widget__threads-overlay--open" : ""}`,
454
+ children: [
455
+ /* @__PURE__ */ jsx(
456
+ "button",
457
+ {
458
+ type: "button",
459
+ className: "ds-chat-widget__rail-scrim",
460
+ "aria-label": "Close the chat history",
461
+ tabIndex: threadsOpen ? 0 : -1,
462
+ onClick: closeThreads
463
+ }
464
+ ),
465
+ /* @__PURE__ */ jsx("div", { className: "ds-chat-widget__threads-sheet", children: renderThreads({ overlay: true, close: closeThreads }) })
466
+ ]
467
+ }
468
+ ),
469
+ renderAside && !asideWide && /* @__PURE__ */ jsxs(
470
+ "div",
471
+ {
472
+ className: `ds-chat-widget__aside-overlay ${asideOpen ? "ds-chat-widget__aside-overlay--open" : ""}`,
473
+ children: [
474
+ /* @__PURE__ */ jsx(
475
+ "button",
476
+ {
477
+ type: "button",
478
+ className: "ds-chat-widget__rail-scrim",
479
+ "aria-label": "Close the agent panel",
480
+ tabIndex: asideOpen ? 0 : -1,
481
+ onClick: closeAside
482
+ }
483
+ ),
484
+ /* @__PURE__ */ jsxs("div", { className: "ds-chat-widget__aside-sheet", id: asideId, children: [
485
+ /* @__PURE__ */ jsx("span", { className: "ds-chat-widget__aside-grabber", "aria-hidden": "true" }),
486
+ /* @__PURE__ */ jsx("div", { className: "ds-chat-widget__aside-sheet-body", children: renderAside({ overlay: true, close: closeAside }) })
487
+ ] })
488
+ ]
489
+ }
490
+ )
491
+ ] });
492
+ }
493
+ export {
494
+ ChatWidget
495
+ };
@@ -0,0 +1,13 @@
1
+ import { DemoWelcomeVariant } from './demo-fixtures';
2
+ /** A board of Widgets for the welcome slot: the chat as a home page. */
3
+ export declare function DemoBoard({ onAsk }: {
4
+ onAsk: (prompt: string) => void;
5
+ }): import("react").JSX.Element;
6
+ /**
7
+ * The welcome slot's content for a variant, or nothing for the two that
8
+ * leave the slot empty (`starters` is the widget's own, `none` is none).
9
+ */
10
+ export declare function DemoWelcome({ variant, onAsk, }: {
11
+ variant: DemoWelcomeVariant;
12
+ onAsk: (prompt: string) => void;
13
+ }): import("react").JSX.Element | null;