rift-ds 1.7.0 → 1.9.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 (131) hide show
  1. package/README.md +31 -8
  2. package/bin/rift-ds.mjs +142 -29
  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 +24 -0
  14. package/chat/ChatAgentPane.js +28 -0
  15. package/chat/ChatArtifactCard.d.ts +13 -0
  16. package/chat/ChatArtifactCard.js +21 -0
  17. package/chat/ChatArtifactPane.d.ts +18 -0
  18. package/chat/ChatArtifactPane.js +43 -0
  19. package/chat/ChatLauncher.d.ts +64 -0
  20. package/chat/ChatLauncher.js +347 -0
  21. package/chat/ChatProvider.d.ts +55 -0
  22. package/chat/ChatProvider.js +196 -0
  23. package/chat/ChatTabs.d.ts +13 -0
  24. package/chat/ChatTabs.js +24 -0
  25. package/chat/ChatThreads.d.ts +26 -0
  26. package/chat/ChatThreads.js +55 -0
  27. package/chat/ChatWidget.css +1259 -0
  28. package/chat/ChatWidget.d.ts +115 -0
  29. package/chat/ChatWidget.js +709 -0
  30. package/chat/DemoWelcome.d.ts +13 -0
  31. package/chat/DemoWelcome.js +131 -0
  32. package/chat/FollowupSuggestions.d.ts +23 -0
  33. package/chat/FollowupSuggestions.js +34 -0
  34. package/chat/ResponseActions.d.ts +18 -0
  35. package/chat/ResponseActions.js +79 -0
  36. package/chat/TurnContent.d.ts +11 -0
  37. package/chat/TurnContent.js +19 -0
  38. package/chat/artifacts.d.ts +5 -0
  39. package/chat/artifacts.js +14 -0
  40. package/chat/context.d.ts +82 -0
  41. package/chat/context.js +29 -0
  42. package/chat/demo-agent-panes.d.ts +5 -0
  43. package/chat/demo-agent-panes.js +49 -0
  44. package/chat/demo-agent.d.ts +3 -0
  45. package/chat/demo-agent.js +28 -0
  46. package/chat/demo-artifacts.d.ts +6 -0
  47. package/chat/demo-artifacts.js +80 -0
  48. package/chat/demo-card-components.d.ts +48 -0
  49. package/chat/demo-card-components.js +66 -0
  50. package/chat/demo-cards.d.ts +32 -0
  51. package/chat/demo-cards.js +59 -0
  52. package/chat/demo-fixtures.d.ts +20 -0
  53. package/chat/demo-fixtures.js +113 -0
  54. package/chat/demo-script.d.ts +6 -0
  55. package/chat/demo-script.js +237 -0
  56. package/chat/demo.css +52 -0
  57. package/chat/demo.d.ts +10 -0
  58. package/chat/demo.js +28 -0
  59. package/chat/followups.d.ts +41 -0
  60. package/chat/followups.js +55 -0
  61. package/chat/greeting.d.ts +11 -0
  62. package/chat/greeting.js +21 -0
  63. package/chat/index.d.ts +37 -0
  64. package/chat/index.js +90 -0
  65. package/chat/placement.d.ts +48 -0
  66. package/chat/placement.js +39 -0
  67. package/chat/queries.d.ts +18 -0
  68. package/chat/queries.js +26 -0
  69. package/chat/registry.d.ts +25 -0
  70. package/chat/registry.js +9 -0
  71. package/chat/registry.json.d.ts +74 -0
  72. package/chat/registry.json.js +11 -0
  73. package/chat/server.d.ts +217 -0
  74. package/chat/server.js +488 -0
  75. package/chat/sim.d.ts +71 -0
  76. package/chat/sim.js +125 -0
  77. package/chat/starters.d.ts +15 -0
  78. package/chat/starters.js +38 -0
  79. package/chat/suggestions.d.ts +22 -0
  80. package/chat/suggestions.js +9 -0
  81. package/chat/threads.d.ts +73 -0
  82. package/chat/threads.js +270 -0
  83. package/chat/transport.d.ts +49 -0
  84. package/chat/transport.js +105 -0
  85. package/chat/types.d.ts +246 -0
  86. package/chat/types.js +18 -0
  87. package/chat/upstash.d.ts +63 -0
  88. package/chat/upstash.js +147 -0
  89. package/chat/useChat.d.ts +64 -0
  90. package/chat/useChat.js +241 -0
  91. package/cli/templates/chat/app/api/chat/feedback/route.d.ts +3 -0
  92. package/cli/templates/chat/app/api/chat/followups/route.d.ts +3 -0
  93. package/cli/templates/chat/app/api/chat/guardrails.d.ts +10 -0
  94. package/cli/templates/chat/app/api/chat/route.d.ts +4 -0
  95. package/cli/templates/chat/components/chat/ChatMount.d.ts +1 -0
  96. package/cli/templates/chat/components/chat/models.d.ts +11 -0
  97. package/components/AgentRail/AgentRail.css +14 -5
  98. package/components/AgentRail/AgentRail.d.ts +2 -0
  99. package/components/AgentRail/AgentRail.js +3 -2
  100. package/components/ArtifactCard/ArtifactCard.css +106 -0
  101. package/components/ArtifactCard/ArtifactCard.d.ts +27 -0
  102. package/components/ArtifactCard/ArtifactCard.js +29 -0
  103. package/components/ArtifactPanel/ArtifactPanel.css +159 -0
  104. package/components/ArtifactPanel/ArtifactPanel.d.ts +70 -0
  105. package/components/ArtifactPanel/ArtifactPanel.js +131 -0
  106. package/components/ShaderField/ShaderField.d.ts +3 -2
  107. package/components/ShaderField/useShaderField.d.ts +2 -1
  108. package/components/ShaderField/useShaderField.js +6 -6
  109. package/components/ThreadPanel/ThreadPanel.css +87 -33
  110. package/components/ThreadPanel/ThreadPanel.js +21 -10
  111. package/components/registry.json +16 -0
  112. package/components/registry.json.d.ts +16 -0
  113. package/components/registry.json.js +1 -1
  114. package/fonts/material-symbols.css +2 -1
  115. package/index.d.ts +2 -0
  116. package/index.js +4 -0
  117. package/package.json +39 -2
  118. package/tokens/presets/coral.css +18 -2
  119. package/tokens/presets/forest.css +18 -2
  120. package/tokens/presets/gold.css +18 -2
  121. package/tokens/presets/mono.css +18 -2
  122. package/tokens/presets/pink.css +18 -2
  123. package/tokens/presets/terminal.css +18 -2
  124. package/tokens/presets/violet.css +18 -2
  125. package/tokens/presets/warm.css +18 -2
  126. package/tokens/presets/zest.css +18 -2
  127. package/tokens/registry.json +6 -0
  128. package/tokens/registry.json.d.ts +6 -0
  129. package/tokens/registry.json.js +1 -1
  130. package/tokens/tokens-dark.css +10 -0
  131. package/tokens/tokens-light.css +10 -0
@@ -0,0 +1,709 @@
1
+ "use client";
2
+ import { jsx, jsxs, Fragment } from "react/jsx-runtime";
3
+ import { useSyncExternalStore, useRef, useEffect, useState, useId, useMemo } 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 { cardArtifact } from "./artifacts.js";
20
+ import { ChatAgentPane } from "./ChatAgentPane.js";
21
+ import { ChatArtifactPane } from "./ChatArtifactPane.js";
22
+ import { ChatTabs } from "./ChatTabs.js";
23
+ import { ChatThreads } from "./ChatThreads.js";
24
+ import { useChatWidget, ArtifactSeatContext } from "./context.js";
25
+ import { subscribeClock, readGreeting, serverGreeting } from "./greeting.js";
26
+ import { CHAT_PLACEMENTS, chatPlacement } from "./placement.js";
27
+ import { ASIDE_WITH_THREADS_MIN_WIDTH, ARTIFACT_MIN_COLUMN_WIDTH, THREADS_RAIL_MIN_WIDTH, ASIDE_RAIL_MIN_WIDTH } from "./queries.js";
28
+ import { fitsChip } from "./suggestions.js";
29
+ import { TurnCard } from "./TurnContent.js";
30
+ import "./ChatWidget.css";
31
+ const DEFAULT_MAX_ATTACHMENTS = 10;
32
+ const DEFAULT_MAX_ATTACHMENT_BYTES = 50 * 1024 * 1024;
33
+ const LONG_PASTE_CHARS = 1200;
34
+ const formatBytes = (bytes) => bytes >= 1024 * 1024 * 1024 ? `${Math.round(bytes / (1024 * 1024 * 1024))} GB` : `${Math.round(bytes / (1024 * 1024))} MB`;
35
+ const withAgentMark = (profile, title) => profile.avatar || profile.avatarSrc ? profile : { ...profile, avatar: /* @__PURE__ */ jsx(PixelAvatar, { name: title, size: "lg", label: title }) };
36
+ function ChatWidget({
37
+ fullscreenEnabled = true,
38
+ closeEnabled = true,
39
+ placementEnabled = false,
40
+ compact = false,
41
+ phone = false,
42
+ title = "Assistant",
43
+ placeholder = "Ask anything",
44
+ showStarters = true,
45
+ logo = null,
46
+ tagline = "How can I help you today?",
47
+ starters: startersInput,
48
+ startersKey = "",
49
+ composerActions,
50
+ models,
51
+ defaultModel,
52
+ lockedModelDescription = "Paused for today to stay in budget.",
53
+ fileAttachments = false,
54
+ maxAttachments = DEFAULT_MAX_ATTACHMENTS,
55
+ maxAttachmentBytes = DEFAULT_MAX_ATTACHMENT_BYTES,
56
+ mentions,
57
+ threads,
58
+ threadsProps,
59
+ tabs,
60
+ aside,
61
+ agent,
62
+ artifacts = false,
63
+ contextLabel,
64
+ widgets,
65
+ widgetsLayout = "fit",
66
+ widgetsWidth = "column",
67
+ disclaimer = "Answers may be wrong."
68
+ }) {
69
+ const {
70
+ turns,
71
+ live,
72
+ streaming,
73
+ modelPolicy,
74
+ chosenModel,
75
+ setChosenModel,
76
+ send,
77
+ stop,
78
+ reset,
79
+ open,
80
+ setOpen,
81
+ view,
82
+ setView,
83
+ placement,
84
+ setPlacement,
85
+ draft,
86
+ setDraft,
87
+ starterSeed,
88
+ starterRevealKey,
89
+ setStarterRevealKey,
90
+ artifact,
91
+ openArtifact,
92
+ closeArtifact
93
+ } = useChatWidget();
94
+ const lockedValues = modelPolicy?.locked ?? [];
95
+ const pick = chosenModel && !lockedValues.includes(chosenModel) ? chosenModel : null;
96
+ const pickerValue = pick ?? modelPolicy?.default ?? defaultModel ?? models?.[0]?.value;
97
+ const pickerModels = (models ?? []).map((option) => ({
98
+ value: option.value,
99
+ label: option.label,
100
+ description: lockedValues.includes(option.value) ? lockedModelDescription : option.description,
101
+ disabled: lockedValues.includes(option.value)
102
+ }));
103
+ const greeting = useSyncExternalStore(subscribeClock, readGreeting, serverGreeting);
104
+ const starters = (startersInput ?? []).filter((starter) => fitsChip(starter.label));
105
+ const composerRef = useRef(null);
106
+ const focusComposer = () => composerRef.current?.focus({ preventScroll: true });
107
+ useEffect(() => {
108
+ if (open) focusComposer();
109
+ }, [open]);
110
+ const queue = useAttachments({ maxCount: maxAttachments, maxSize: maxAttachmentBytes });
111
+ const attachHint = `Up to ${formatBytes(maxAttachmentBytes)} each · ${maxAttachments} files max`;
112
+ const handleSubmit = (value) => {
113
+ if (fileAttachments && queue.items.length > 0) {
114
+ const ready = queue.items.filter((item) => item.status === "ready");
115
+ if (send(value, { attachments: ready })) {
116
+ queue.take();
117
+ setDraft("");
118
+ }
119
+ } else if (send(value)) setDraft("");
120
+ focusComposer();
121
+ };
122
+ const isFull = view === "full";
123
+ const isEmpty = turns.length === 0 && !live;
124
+ const [stagedKey, setStagedKey] = useState(null);
125
+ const startersThinking = open && isEmpty && starterRevealKey !== startersKey;
126
+ const startersEntrance = startersThinking || stagedKey === startersKey;
127
+ useEffect(() => {
128
+ if (!(open && isEmpty && starterRevealKey !== startersKey)) return;
129
+ const timer = setTimeout(() => {
130
+ setStagedKey(startersKey);
131
+ setStarterRevealKey(startersKey);
132
+ }, MOTION_SUGGESTIONS_THINK_MS);
133
+ return () => clearTimeout(timer);
134
+ }, [open, isEmpty, starterRevealKey, setStarterRevealKey, startersKey]);
135
+ const renderThreads = threads === true ? (ctx) => /* @__PURE__ */ jsx(ChatThreads, { ...threadsProps, logoText: threadsProps?.logoText ?? title, ...ctx }) : threads || void 0;
136
+ const renderAside = aside ?? (agent ? (ctx) => /* @__PURE__ */ jsx(
137
+ ChatAgentPane,
138
+ {
139
+ profile: withAgentMark(agent.profile ?? { name: title, status: "Connected" }, title),
140
+ tabs: agent.tabs,
141
+ overlay: ctx.overlay,
142
+ onClose: ctx.close
143
+ }
144
+ ) : void 0);
145
+ const tabsNode = tabs === true ? /* @__PURE__ */ jsx(ChatTabs, {}) : tabs || null;
146
+ const hasRails = Boolean(renderThreads || renderAside || artifacts);
147
+ const [threadsOpen, setThreadsOpen] = useState(false);
148
+ const [asideOpen, setAsideOpen] = useState(Boolean(agent?.defaultOpen));
149
+ const railsHostRef = useRef(null);
150
+ const [threadsWide, setThreadsWide] = useState(false);
151
+ const [asideWide, setAsideWide] = useState(false);
152
+ const [asideRoomy, setAsideRoomy] = useState(false);
153
+ const [artifactEmptyOpen, setArtifactEmptyOpen] = useState(false);
154
+ const [artifactExpandedWanted, setArtifactExpanded] = useState(false);
155
+ const wasAsideWideRef = useRef(true);
156
+ useEffect(() => {
157
+ if (!hasRails) return;
158
+ const host = railsHostRef.current;
159
+ if (!host) return;
160
+ const measure = () => {
161
+ const width = host.clientWidth;
162
+ const wide = width >= THREADS_RAIL_MIN_WIDTH;
163
+ const asideRoom = width >= ASIDE_RAIL_MIN_WIDTH;
164
+ setThreadsWide(wide);
165
+ setAsideWide(asideRoom);
166
+ setAsideRoomy(width >= ASIDE_WITH_THREADS_MIN_WIDTH);
167
+ if (wide) setThreadsOpen(false);
168
+ if (wasAsideWideRef.current && !asideRoom) {
169
+ setAsideOpen(false);
170
+ setArtifactEmptyOpen(false);
171
+ setArtifactExpanded(false);
172
+ closeArtifact();
173
+ }
174
+ wasAsideWideRef.current = asideRoom;
175
+ };
176
+ measure();
177
+ const observer = new ResizeObserver(measure);
178
+ observer.observe(host);
179
+ return () => observer.disconnect();
180
+ }, [hasRails, closeArtifact]);
181
+ const closeThreads = () => setThreadsOpen(false);
182
+ const artifactId = useId();
183
+ const artifactShown = artifacts && (artifact !== null || artifactEmptyOpen);
184
+ const [heldArtifact, setHeldArtifact] = useState(artifact);
185
+ if (artifactShown && artifact !== heldArtifact) setHeldArtifact(artifact);
186
+ const artifactInline = artifactShown && asideWide;
187
+ const artifactExpanded = artifactInline && artifactExpandedWanted;
188
+ const closeArtifactPanel = () => {
189
+ setArtifactExpanded(false);
190
+ setArtifactEmptyOpen(false);
191
+ closeArtifact();
192
+ };
193
+ const [artifactWidth, setArtifactWidth] = useState(null);
194
+ const [artifactResizing, setArtifactResizing] = useState(false);
195
+ const artifactRailRef = useRef(null);
196
+ const artifactDragRef = useRef(null);
197
+ const beginArtifactResize = (e) => {
198
+ if (e.pointerType === "mouse" && e.button !== 0 || artifactDragRef.current) return;
199
+ const rail = artifactRailRef.current;
200
+ const column = railsHostRef.current?.querySelector(":scope > .ds-chat-widget");
201
+ if (!rail || !column) return;
202
+ const width = rail.getBoundingClientRect().width;
203
+ const spare = column.getBoundingClientRect().width - ARTIFACT_MIN_COLUMN_WIDTH;
204
+ artifactDragRef.current = { startX: e.clientX, width, max: width + Math.max(0, spare), next: width };
205
+ e.currentTarget.setPointerCapture(e.pointerId);
206
+ setArtifactResizing(true);
207
+ };
208
+ const moveArtifactResize = (e) => {
209
+ const drag = artifactDragRef.current;
210
+ if (!drag) return;
211
+ const grown = drag.width - (e.clientX - drag.startX);
212
+ drag.next = Math.round(Math.min(drag.max, Math.max(ARTIFACT_MIN_COLUMN_WIDTH, grown)));
213
+ railsHostRef.current?.style.setProperty("--ds-chat-artifact-width", `${drag.next}px`);
214
+ };
215
+ const endArtifactResize = () => {
216
+ const drag = artifactDragRef.current;
217
+ if (!drag) return;
218
+ artifactDragRef.current = null;
219
+ setArtifactWidth(drag.next);
220
+ setArtifactResizing(false);
221
+ };
222
+ const latestArtifact = useMemo(() => {
223
+ for (let index = turns.length - 1; index >= 0; index -= 1) {
224
+ const found = cardArtifact(turns[index].card);
225
+ if (found) return found;
226
+ }
227
+ return null;
228
+ }, [turns]);
229
+ const seenArtifactTurns = useRef(null);
230
+ const asideWideRef = useRef(asideWide);
231
+ useEffect(() => {
232
+ asideWideRef.current = asideWide;
233
+ }, [asideWide]);
234
+ useEffect(() => {
235
+ if (!artifacts) return;
236
+ const carrying = turns.filter((turn) => cardArtifact(turn.card));
237
+ if (seenArtifactTurns.current === null) {
238
+ seenArtifactTurns.current = new Set(carrying.map((turn) => turn.id));
239
+ return;
240
+ }
241
+ for (const turn of carrying) {
242
+ if (seenArtifactTurns.current.has(turn.id)) continue;
243
+ seenArtifactTurns.current.add(turn.id);
244
+ const arrived = cardArtifact(turn.card);
245
+ if (arrived && asideWideRef.current) openArtifact(arrived);
246
+ }
247
+ }, [artifacts, turns, openArtifact]);
248
+ const trailingUp = artifactInline || Boolean(renderAside) && asideOpen && asideWide;
249
+ const threadsInline = threadsWide && !(trailingUp && (!asideRoomy || artifactExpanded));
250
+ const asideRailRef = useRef(null);
251
+ const asideMounted = Boolean(renderAside) && asideWide;
252
+ useEffect(() => {
253
+ if (!asideMounted) return;
254
+ const rail = asideRailRef.current;
255
+ const card = rail?.firstElementChild;
256
+ const pane = card?.firstElementChild;
257
+ if (!rail || !card || !pane) return;
258
+ const measure = () => {
259
+ const width = pane.offsetWidth + (card.offsetWidth - card.clientWidth);
260
+ if (pane.offsetWidth > 0) rail.style.setProperty("--ds-chat-aside-width", `${width}px`);
261
+ else rail.style.removeProperty("--ds-chat-aside-width");
262
+ };
263
+ measure();
264
+ const observer = new ResizeObserver(measure);
265
+ observer.observe(pane);
266
+ return () => observer.disconnect();
267
+ }, [asideMounted]);
268
+ const asideId = useId();
269
+ const asideShown = Boolean(renderAside) && asideOpen && !artifactShown;
270
+ const closeAside = () => setAsideOpen(false);
271
+ const toggleAside = () => {
272
+ if (artifactShown) {
273
+ closeArtifactPanel();
274
+ setAsideOpen(true);
275
+ return;
276
+ }
277
+ setAsideOpen((current) => !current);
278
+ };
279
+ const startersNode = isEmpty && showStarters && starters.length > 0 && /* @__PURE__ */ jsx("div", { className: "ds-chat-widget__starters", children: /* @__PURE__ */ jsx(
280
+ PromptSuggestions,
281
+ {
282
+ layout: "stack",
283
+ ariaLabel: "Conversation starters",
284
+ pending: startersThinking,
285
+ entrance: startersEntrance,
286
+ suggestions: starters,
287
+ onValueChange: (id) => {
288
+ const starter = starters.find((s) => s.id === id);
289
+ if (starter && send(starter.label)) focusComposer();
290
+ }
291
+ }
292
+ ) });
293
+ const widgetsNode = isEmpty && widgets && /* @__PURE__ */ jsx("div", { className: "ds-chat-widget__widgets", children: widgets });
294
+ const trailingInline = asideShown && asideWide || artifactInline;
295
+ const actionsFloat = (Boolean(renderAside) || artifacts) && asideWide;
296
+ const showNewChat = !(renderThreads && threadsWide);
297
+ const actionCount = Number(showNewChat) + Number(Boolean(renderAside)) + Number(artifacts) + Number(placementEnabled) + Number(fullscreenEnabled) + Number(closeEnabled);
298
+ const headerActions = /* @__PURE__ */ jsxs(Fragment, { children: [
299
+ showNewChat && /* @__PURE__ */ jsx(
300
+ CircularButton,
301
+ {
302
+ icon: "edit_square",
303
+ variant: "tertiary",
304
+ ariaLabel: "New chat",
305
+ tooltipPosition: "bottom",
306
+ onClick: () => {
307
+ reset();
308
+ setDraft("");
309
+ focusComposer();
310
+ }
311
+ }
312
+ ),
313
+ renderAside && /* @__PURE__ */ jsx(
314
+ CircularButton,
315
+ {
316
+ icon: /* @__PURE__ */ jsx(PixelAvatar, { name: title, size: "sm" }),
317
+ variant: "tertiary",
318
+ className: asideShown ? "ds-chat-widget__agent-toggle--on" : void 0,
319
+ ariaLabel: asideShown ? "Hide the agent panel" : "Show the agent panel",
320
+ tooltipPosition: "bottom",
321
+ "aria-expanded": asideShown,
322
+ "aria-controls": asideShown ? asideId : void 0,
323
+ onClick: toggleAside
324
+ }
325
+ ),
326
+ artifacts && /* @__PURE__ */ jsx(
327
+ CircularButton,
328
+ {
329
+ icon: artifactShown ? "right_panel_close" : "right_panel_open",
330
+ variant: "tertiary",
331
+ className: artifactShown ? "ds-chat-widget__agent-toggle--on" : void 0,
332
+ ariaLabel: artifactShown ? "Hide the artifact panel" : "Show the artifact panel",
333
+ tooltipPosition: "bottom",
334
+ "aria-expanded": artifactShown,
335
+ "aria-controls": artifactShown ? artifactId : void 0,
336
+ onClick: () => {
337
+ if (artifactShown) closeArtifactPanel();
338
+ else if (latestArtifact) openArtifact(latestArtifact);
339
+ else setArtifactEmptyOpen(true);
340
+ }
341
+ }
342
+ ),
343
+ placementEnabled && /* @__PURE__ */ jsx(
344
+ "span",
345
+ {
346
+ className: "ds-chat-widget__placement-menu",
347
+ onKeyDown: (event) => {
348
+ if (event.key === "Escape" && event.currentTarget.querySelector('[aria-expanded="true"]')) {
349
+ event.stopPropagation();
350
+ }
351
+ },
352
+ children: /* @__PURE__ */ jsx(
353
+ DropdownMenu,
354
+ {
355
+ align: "end",
356
+ trigger: /* @__PURE__ */ jsx(
357
+ CircularButton,
358
+ {
359
+ icon: chatPlacement(placement).icon,
360
+ variant: "tertiary",
361
+ ariaLabel: "Chat position",
362
+ tooltipPosition: "bottom",
363
+ disabled: isFull
364
+ }
365
+ ),
366
+ items: [
367
+ {
368
+ type: "group",
369
+ label: "Chat position",
370
+ items: CHAT_PLACEMENTS.map(({ id, label, icon }) => ({
371
+ label,
372
+ icon,
373
+ selected: placement === id,
374
+ onClick: () => setPlacement(id)
375
+ }))
376
+ }
377
+ ]
378
+ }
379
+ )
380
+ }
381
+ ),
382
+ fullscreenEnabled && /* @__PURE__ */ jsx(
383
+ CircularButton,
384
+ {
385
+ icon: isFull ? "close_fullscreen" : "open_in_full",
386
+ variant: "tertiary",
387
+ ariaLabel: isFull ? "Exit full screen" : "Enter full screen",
388
+ tooltipPosition: "bottom",
389
+ onClick: () => setView(isFull ? "panel" : "full")
390
+ }
391
+ ),
392
+ closeEnabled && /* @__PURE__ */ jsx(
393
+ CircularButton,
394
+ {
395
+ icon: "close",
396
+ variant: "tertiary",
397
+ ariaLabel: "Close chat",
398
+ tooltipPosition: "bottom",
399
+ onClick: () => setOpen(false)
400
+ }
401
+ )
402
+ ] });
403
+ const ChatRoot = fileAttachments ? AttachmentDropZone : "div";
404
+ const dropProps = fileAttachments ? { onFilesSelected: queue.add, hint: attachHint } : {};
405
+ const [rootScrolling, setRootScrolling] = useState(false);
406
+ const rootSettleTimer = useRef(void 0);
407
+ const handleRootScroll = () => {
408
+ if (widgetsLayout !== "scroll") return;
409
+ setRootScrolling(true);
410
+ window.clearTimeout(rootSettleTimer.current);
411
+ rootSettleTimer.current = window.setTimeout(() => setRootScrolling(false), MOTION_SCROLL_SETTLE_MS);
412
+ };
413
+ useEffect(() => () => window.clearTimeout(rootSettleTimer.current), []);
414
+ const chatColumn = /* @__PURE__ */ jsxs(
415
+ ChatRoot,
416
+ {
417
+ ...dropProps,
418
+ className: `ds-chat-widget ${renderThreads ? "ds-chat-widget--with-threads" : ""}`,
419
+ "data-compact": compact || void 0,
420
+ "data-phone": phone || void 0,
421
+ "data-widgets": widgetsNode ? widgetsLayout : void 0,
422
+ "data-widgets-wide": widgetsNode && widgetsWidth === "wide" ? "" : void 0,
423
+ "data-scrolling": widgetsNode && widgetsLayout === "scroll" && rootScrolling || void 0,
424
+ onScroll: handleRootScroll,
425
+ "data-lifted-actions": trailingInline || void 0,
426
+ onKeyDown: (e) => {
427
+ if (e.key === "Escape" && (threadsOpen || asideShown && !asideWide || artifactShown && !asideWide)) {
428
+ e.stopPropagation();
429
+ if (threadsOpen) closeThreads();
430
+ else if (artifactShown) closeArtifactPanel();
431
+ else closeAside();
432
+ return;
433
+ }
434
+ if (e.key === "Escape" && isFull) {
435
+ e.stopPropagation();
436
+ setView("panel");
437
+ }
438
+ },
439
+ onClick: (e) => {
440
+ if (!isEmpty) return;
441
+ const target = e.target;
442
+ if (target.closest("button, a, input, textarea, [role='button']")) return;
443
+ if (window.getSelection()?.toString()) return;
444
+ focusComposer();
445
+ },
446
+ children: [
447
+ /* @__PURE__ */ jsxs("div", { className: "ds-chat-widget__top", children: [
448
+ /* @__PURE__ */ jsx(
449
+ ChatHeader,
450
+ {
451
+ title: /* @__PURE__ */ jsxs("span", { className: "ds-chat-widget__brand", children: [
452
+ renderThreads && !threadsInline && /* @__PURE__ */ jsx(
453
+ CircularButton,
454
+ {
455
+ icon: "menu",
456
+ variant: "tertiary",
457
+ ariaLabel: "Open the chat history",
458
+ tooltipPosition: "bottom",
459
+ onClick: () => setThreadsOpen(true)
460
+ }
461
+ ),
462
+ logo && /* @__PURE__ */ jsx("img", { src: logo, alt: "", width: 20, height: 20 }),
463
+ /* @__PURE__ */ jsx("span", { className: "ds-chat-widget__brand-name", children: title })
464
+ ] }),
465
+ actions: (
466
+ /* With the agent rail seated inline the cluster moves to the
467
+ widget's own right edge (see headerActions). */
468
+ !actionsFloat ? headerActions : trailingInline ? void 0 : /* @__PURE__ */ jsx(
469
+ "span",
470
+ {
471
+ className: "ds-chat-widget__actions-spacer",
472
+ style: { "--ds-chat-action-count": actionCount },
473
+ "aria-hidden": "true"
474
+ }
475
+ )
476
+ )
477
+ }
478
+ ),
479
+ tabsNode && /* @__PURE__ */ jsx("div", { className: "ds-chat-widget__tabs-row", children: tabsNode }),
480
+ /* @__PURE__ */ jsxs("div", { className: "ds-chat-widget__body", children: [
481
+ /* @__PURE__ */ jsx(ArtifactSeatContext.Provider, { value: artifacts, children: /* @__PURE__ */ jsx(ChatThread, { className: "ds-chat-widget__thread", children: [
482
+ ...turns.map(
483
+ (turn) => turn.bare ? (
484
+ /* Conversation furniture (a day marker): the injected
485
+ content stands alone in the thread, no message chrome. */
486
+ /* @__PURE__ */ jsx("div", { children: turn.content }, turn.id)
487
+ ) : turn.role === "user" ? (
488
+ /* Files ride above the bubble on the sender's side; a turn
489
+ of files alone draws no bubble. */
490
+ /* @__PURE__ */ jsx(
491
+ ChatMessage,
492
+ {
493
+ role: "user",
494
+ attachments: turn.attachments ? /* @__PURE__ */ jsx(AttachmentGroup, { align: "end", items: turn.attachments }) : /* @__PURE__ */ jsxs(Fragment, { children: [
495
+ turn.content,
496
+ turn.card && /* @__PURE__ */ jsx(TurnCard, { card: turn.card })
497
+ ] }),
498
+ children: mentions && turn.text ? /* @__PURE__ */ jsx(MentionText, { text: turn.text, sources: mentions }) : turn.text
499
+ },
500
+ turn.id
501
+ )
502
+ ) : /* @__PURE__ */ jsx(AssistantTurn, { turn }, turn.id)
503
+ ),
504
+ ...live ? [/* @__PURE__ */ jsx(AssistantTurn, { live }, live.id)] : []
505
+ ] }) }),
506
+ isEmpty && /* @__PURE__ */ jsx("div", { className: "ds-chat-widget__welcome", children: /* @__PURE__ */ jsxs("div", { className: "ds-chat-widget__greeting", children: [
507
+ /* @__PURE__ */ jsx("p", { className: "ds-chat-widget__hello", children: greeting }),
508
+ /* @__PURE__ */ jsx("p", { className: "ds-chat-widget__ask", children: tagline })
509
+ ] }) })
510
+ ] })
511
+ ] }),
512
+ phone && startersNode,
513
+ phone && widgetsLayout === "fit" && widgetsNode,
514
+ /* @__PURE__ */ jsx("footer", { className: "ds-chat-widget__footer", children: /* @__PURE__ */ jsx("div", { className: "ds-chat-widget__composer", children: /* @__PURE__ */ jsx(
515
+ Composer,
516
+ {
517
+ ref: composerRef,
518
+ placeholder,
519
+ sendLabel: "Send",
520
+ aiGlow: true,
521
+ value: draft,
522
+ onValueChange: setDraft,
523
+ onSubmit: handleSubmit,
524
+ streaming,
525
+ onStop: stop,
526
+ context: contextLabel ? /* @__PURE__ */ jsxs(Fragment, { children: [
527
+ "Looking at “",
528
+ contextLabel,
529
+ "”"
530
+ ] }) : void 0,
531
+ contextIcon: "description",
532
+ contextPlacement: "above",
533
+ mentions,
534
+ ...fileAttachments ? {
535
+ files: queue.items,
536
+ onFilesSelected: queue.add,
537
+ onFileRemove: queue.remove,
538
+ pasteThreshold: LONG_PASTE_CHARS,
539
+ onPasteAsAttachment: (text) => queue.addText(text)
540
+ } : {},
541
+ actions: (
542
+ /* A host can slot its own leading actions. The default is the
543
+ model picker over the host's model list: the pick rides on
544
+ the next send, the server clamps it to the day's budget, and
545
+ the selection follows the server's default until the visitor
546
+ chooses. */
547
+ composerActions ?? (pickerModels.length > 0 ? /* @__PURE__ */ jsx(
548
+ ModelPicker,
549
+ {
550
+ models: pickerModels,
551
+ value: pickerValue,
552
+ onValueChange: setChosenModel,
553
+ placement: "top",
554
+ "aria-label": "Choose a model"
555
+ }
556
+ ) : void 0)
557
+ )
558
+ }
559
+ ) }) }),
560
+ /* @__PURE__ */ jsxs(
561
+ "div",
562
+ {
563
+ className: `ds-chat-widget__bottom ${isEmpty ? "ds-chat-widget__bottom--welcome" : ""} ${widgetsNode ? "ds-chat-widget__bottom--widgets" : ""}`,
564
+ children: [
565
+ !phone && startersNode,
566
+ (!phone || widgetsLayout === "scroll") && widgetsNode,
567
+ disclaimer && /* @__PURE__ */ jsx("div", { className: "ds-chat-widget__disclaimer-row", children: /* @__PURE__ */ jsx("p", { className: "ds-chat-widget__disclaimer", children: disclaimer }) })
568
+ ]
569
+ }
570
+ )
571
+ ]
572
+ }
573
+ );
574
+ if (!hasRails) return chatColumn;
575
+ return /* @__PURE__ */ jsxs(
576
+ "div",
577
+ {
578
+ ref: railsHostRef,
579
+ className: "ds-chat-widget__rails",
580
+ "data-artifact-expanded": artifactExpanded || void 0,
581
+ "data-artifact-sized": artifactWidth !== null || artifactResizing || void 0,
582
+ "data-artifact-resizing": artifactResizing || void 0,
583
+ style: artifactWidth !== null ? { "--ds-chat-artifact-width": `${artifactWidth}px` } : void 0,
584
+ onKeyDown: (event) => {
585
+ if (event.key === "Escape" && artifactExpanded) {
586
+ event.stopPropagation();
587
+ setArtifactExpanded(false);
588
+ }
589
+ },
590
+ children: [
591
+ renderThreads && threadsInline && /* @__PURE__ */ jsx("div", { className: "ds-chat-widget__threads-rail", children: renderThreads({ overlay: false, close: closeThreads }) }),
592
+ chatColumn,
593
+ actionsFloat && /* @__PURE__ */ jsx("div", { className: "ds-chat-widget__rail-actions", children: headerActions }),
594
+ renderAside && asideWide && /* @__PURE__ */ jsx(
595
+ "div",
596
+ {
597
+ ref: asideRailRef,
598
+ className: `ds-chat-widget__aside-rail ${asideShown ? "ds-chat-widget__aside-rail--open" : ""}`,
599
+ id: asideId,
600
+ inert: !asideShown || void 0,
601
+ children: /* @__PURE__ */ jsx("div", { className: "ds-chat-widget__aside-rail-inner", children: renderAside({ overlay: false, close: closeAside }) })
602
+ }
603
+ ),
604
+ artifactInline && !artifactExpanded && /* @__PURE__ */ jsx(
605
+ "div",
606
+ {
607
+ className: `ds-chat-widget__artifact-grip ${artifactResizing ? "ds-chat-widget__artifact-grip--resizing" : ""}`,
608
+ "aria-hidden": "true",
609
+ onPointerDown: beginArtifactResize,
610
+ onPointerMove: moveArtifactResize,
611
+ onPointerUp: endArtifactResize,
612
+ onPointerCancel: endArtifactResize
613
+ }
614
+ ),
615
+ artifacts && asideWide && /* @__PURE__ */ jsx(
616
+ "div",
617
+ {
618
+ ref: artifactRailRef,
619
+ className: `ds-chat-widget__artifact-rail ${artifactShown ? "ds-chat-widget__artifact-rail--open" : ""}`,
620
+ id: artifactId,
621
+ inert: !artifactShown || void 0,
622
+ children: /* @__PURE__ */ jsx("div", { className: "ds-chat-widget__artifact-rail-inner", children: /* @__PURE__ */ jsx(
623
+ ChatArtifactPane,
624
+ {
625
+ artifact: heldArtifact,
626
+ expanded: artifactExpanded,
627
+ onExpandedChange: setArtifactExpanded,
628
+ onClose: closeArtifactPanel
629
+ }
630
+ ) })
631
+ }
632
+ ),
633
+ artifacts && !asideWide && /* @__PURE__ */ jsxs(
634
+ "div",
635
+ {
636
+ className: `ds-chat-widget__aside-overlay ${artifactShown ? "ds-chat-widget__aside-overlay--open" : ""}`,
637
+ children: [
638
+ /* @__PURE__ */ jsx(
639
+ "button",
640
+ {
641
+ type: "button",
642
+ className: "ds-chat-widget__rail-scrim",
643
+ "aria-label": "Close the artifact panel",
644
+ tabIndex: artifactShown ? 0 : -1,
645
+ onClick: closeArtifactPanel
646
+ }
647
+ ),
648
+ /* @__PURE__ */ jsxs(
649
+ "div",
650
+ {
651
+ className: "ds-chat-widget__aside-sheet ds-chat-widget__aside-sheet--artifact",
652
+ id: artifactId,
653
+ children: [
654
+ /* @__PURE__ */ jsx("span", { className: "ds-chat-widget__aside-grabber", "aria-hidden": "true" }),
655
+ /* @__PURE__ */ jsx("div", { className: "ds-chat-widget__aside-sheet-body", children: /* @__PURE__ */ jsx(ChatArtifactPane, { artifact: heldArtifact, onClose: closeArtifactPanel }) })
656
+ ]
657
+ }
658
+ )
659
+ ]
660
+ }
661
+ ),
662
+ renderThreads && !threadsInline && /* @__PURE__ */ jsxs(
663
+ "div",
664
+ {
665
+ className: `ds-chat-widget__threads-overlay ${threadsOpen ? "ds-chat-widget__threads-overlay--open" : ""}`,
666
+ children: [
667
+ /* @__PURE__ */ jsx(
668
+ "button",
669
+ {
670
+ type: "button",
671
+ className: "ds-chat-widget__rail-scrim",
672
+ "aria-label": "Close the chat history",
673
+ tabIndex: threadsOpen ? 0 : -1,
674
+ onClick: closeThreads
675
+ }
676
+ ),
677
+ /* @__PURE__ */ jsx("div", { className: "ds-chat-widget__threads-sheet", children: renderThreads({ overlay: true, close: closeThreads }) })
678
+ ]
679
+ }
680
+ ),
681
+ renderAside && !asideWide && /* @__PURE__ */ jsxs(
682
+ "div",
683
+ {
684
+ className: `ds-chat-widget__aside-overlay ${asideShown ? "ds-chat-widget__aside-overlay--open" : ""}`,
685
+ children: [
686
+ /* @__PURE__ */ jsx(
687
+ "button",
688
+ {
689
+ type: "button",
690
+ className: "ds-chat-widget__rail-scrim",
691
+ "aria-label": "Close the agent panel",
692
+ tabIndex: asideShown ? 0 : -1,
693
+ onClick: closeAside
694
+ }
695
+ ),
696
+ /* @__PURE__ */ jsxs("div", { className: "ds-chat-widget__aside-sheet", id: asideId, children: [
697
+ /* @__PURE__ */ jsx("span", { className: "ds-chat-widget__aside-grabber", "aria-hidden": "true" }),
698
+ /* @__PURE__ */ jsx("div", { className: "ds-chat-widget__aside-sheet-body", children: renderAside({ overlay: true, close: closeAside }) })
699
+ ] })
700
+ ]
701
+ }
702
+ )
703
+ ]
704
+ }
705
+ );
706
+ }
707
+ export {
708
+ ChatWidget
709
+ };