rift-ds 1.8.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 (51) hide show
  1. package/README.md +3 -3
  2. package/chat/ChatAgentPane.d.ts +8 -5
  3. package/chat/ChatAgentPane.js +4 -1
  4. package/chat/ChatArtifactCard.d.ts +13 -0
  5. package/chat/ChatArtifactCard.js +21 -0
  6. package/chat/ChatArtifactPane.d.ts +18 -0
  7. package/chat/ChatArtifactPane.js +43 -0
  8. package/chat/ChatProvider.d.ts +4 -2
  9. package/chat/ChatProvider.js +15 -0
  10. package/chat/ChatWidget.css +208 -21
  11. package/chat/ChatWidget.d.ts +3 -1
  12. package/chat/ChatWidget.js +287 -73
  13. package/chat/TurnContent.d.ts +3 -1
  14. package/chat/TurnContent.js +9 -2
  15. package/chat/artifacts.d.ts +5 -0
  16. package/chat/artifacts.js +14 -0
  17. package/chat/context.d.ts +18 -1
  18. package/chat/context.js +2 -0
  19. package/chat/demo-artifacts.d.ts +6 -0
  20. package/chat/demo-artifacts.js +80 -0
  21. package/chat/demo-card-components.d.ts +2 -0
  22. package/chat/demo-card-components.js +7 -0
  23. package/chat/demo-script.js +24 -2
  24. package/chat/demo.css +6 -0
  25. package/chat/demo.d.ts +1 -0
  26. package/chat/demo.js +4 -0
  27. package/chat/index.d.ts +6 -3
  28. package/chat/index.js +12 -2
  29. package/chat/queries.d.ts +10 -1
  30. package/chat/queries.js +5 -1
  31. package/chat/registry.json.d.ts +6 -1
  32. package/chat/registry.json.js +1 -1
  33. package/chat/types.d.ts +36 -0
  34. package/chat/types.js +2 -0
  35. package/components/AgentRail/AgentRail.css +14 -5
  36. package/components/AgentRail/AgentRail.d.ts +2 -0
  37. package/components/AgentRail/AgentRail.js +3 -2
  38. package/components/ArtifactCard/ArtifactCard.css +106 -0
  39. package/components/ArtifactCard/ArtifactCard.d.ts +27 -0
  40. package/components/ArtifactCard/ArtifactCard.js +29 -0
  41. package/components/ArtifactPanel/ArtifactPanel.css +159 -0
  42. package/components/ArtifactPanel/ArtifactPanel.d.ts +70 -0
  43. package/components/ArtifactPanel/ArtifactPanel.js +131 -0
  44. package/components/ThreadPanel/ThreadPanel.css +87 -33
  45. package/components/ThreadPanel/ThreadPanel.js +21 -10
  46. package/components/registry.json +16 -0
  47. package/components/registry.json.d.ts +16 -0
  48. package/components/registry.json.js +1 -1
  49. package/index.d.ts +2 -0
  50. package/index.js +4 -0
  51. package/package.json +1 -1
@@ -1,6 +1,6 @@
1
1
  "use client";
2
2
  import { jsx, jsxs, Fragment } from "react/jsx-runtime";
3
- import { useSyncExternalStore, useRef, useEffect, useState, useId } from "react";
3
+ import { useSyncExternalStore, useRef, useEffect, useState, useId, useMemo } from "react";
4
4
  import { AttachmentDropZone } from "../components/Attachment/AttachmentDropZone.js";
5
5
  import { AttachmentGroup } from "../components/Attachment/AttachmentGroup.js";
6
6
  import { useAttachments } from "../components/Attachment/useAttachments.js";
@@ -16,13 +16,15 @@ import { ModelPicker } from "../components/ModelPicker/ModelPicker.js";
16
16
  import { PromptSuggestions } from "../components/PromptSuggestions/PromptSuggestions.js";
17
17
  import { MOTION_SUGGESTIONS_THINK_MS, MOTION_SCROLL_SETTLE_MS } from "../tokens/motion.js";
18
18
  import { AssistantTurn } from "./AssistantTurn.js";
19
+ import { cardArtifact } from "./artifacts.js";
19
20
  import { ChatAgentPane } from "./ChatAgentPane.js";
21
+ import { ChatArtifactPane } from "./ChatArtifactPane.js";
20
22
  import { ChatTabs } from "./ChatTabs.js";
21
23
  import { ChatThreads } from "./ChatThreads.js";
22
- import { useChatWidget } from "./context.js";
24
+ import { useChatWidget, ArtifactSeatContext } from "./context.js";
23
25
  import { subscribeClock, readGreeting, serverGreeting } from "./greeting.js";
24
26
  import { CHAT_PLACEMENTS, chatPlacement } from "./placement.js";
25
- import { THREADS_RAIL_MIN_WIDTH, ASIDE_RAIL_MIN_WIDTH } from "./queries.js";
27
+ import { ASIDE_WITH_THREADS_MIN_WIDTH, ARTIFACT_MIN_COLUMN_WIDTH, THREADS_RAIL_MIN_WIDTH, ASIDE_RAIL_MIN_WIDTH } from "./queries.js";
26
28
  import { fitsChip } from "./suggestions.js";
27
29
  import { TurnCard } from "./TurnContent.js";
28
30
  import "./ChatWidget.css";
@@ -57,6 +59,7 @@ function ChatWidget({
57
59
  tabs,
58
60
  aside,
59
61
  agent,
62
+ artifacts = false,
60
63
  contextLabel,
61
64
  widgets,
62
65
  widgetsLayout = "fit",
@@ -83,7 +86,10 @@ function ChatWidget({
83
86
  setDraft,
84
87
  starterSeed,
85
88
  starterRevealKey,
86
- setStarterRevealKey
89
+ setStarterRevealKey,
90
+ artifact,
91
+ openArtifact,
92
+ closeArtifact
87
93
  } = useChatWidget();
88
94
  const lockedValues = modelPolicy?.locked ?? [];
89
95
  const pick = chosenModel && !lockedValues.includes(chosenModel) ? chosenModel : null;
@@ -132,17 +138,21 @@ function ChatWidget({
132
138
  {
133
139
  profile: withAgentMark(agent.profile ?? { name: title, status: "Connected" }, title),
134
140
  tabs: agent.tabs,
135
- overlay: ctx.overlay
141
+ overlay: ctx.overlay,
142
+ onClose: ctx.close
136
143
  }
137
144
  ) : void 0);
138
145
  const tabsNode = tabs === true ? /* @__PURE__ */ jsx(ChatTabs, {}) : tabs || null;
139
- const hasRails = Boolean(renderThreads || renderAside);
146
+ const hasRails = Boolean(renderThreads || renderAside || artifacts);
140
147
  const [threadsOpen, setThreadsOpen] = useState(false);
141
148
  const [asideOpen, setAsideOpen] = useState(Boolean(agent?.defaultOpen));
142
149
  const railsHostRef = useRef(null);
143
150
  const [threadsWide, setThreadsWide] = useState(false);
144
151
  const [asideWide, setAsideWide] = useState(false);
145
- const wasAsideWideRef = useRef(false);
152
+ const [asideRoomy, setAsideRoomy] = useState(false);
153
+ const [artifactEmptyOpen, setArtifactEmptyOpen] = useState(false);
154
+ const [artifactExpandedWanted, setArtifactExpanded] = useState(false);
155
+ const wasAsideWideRef = useRef(true);
146
156
  useEffect(() => {
147
157
  if (!hasRails) return;
148
158
  const host = railsHostRef.current;
@@ -153,20 +163,119 @@ function ChatWidget({
153
163
  const asideRoom = width >= ASIDE_RAIL_MIN_WIDTH;
154
164
  setThreadsWide(wide);
155
165
  setAsideWide(asideRoom);
166
+ setAsideRoomy(width >= ASIDE_WITH_THREADS_MIN_WIDTH);
156
167
  if (wide) setThreadsOpen(false);
157
- if (wasAsideWideRef.current && !asideRoom) setAsideOpen(false);
168
+ if (wasAsideWideRef.current && !asideRoom) {
169
+ setAsideOpen(false);
170
+ setArtifactEmptyOpen(false);
171
+ setArtifactExpanded(false);
172
+ closeArtifact();
173
+ }
158
174
  wasAsideWideRef.current = asideRoom;
159
175
  };
160
176
  measure();
161
177
  const observer = new ResizeObserver(measure);
162
178
  observer.observe(host);
163
179
  return () => observer.disconnect();
164
- }, [hasRails]);
180
+ }, [hasRails, closeArtifact]);
165
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]);
166
268
  const asideId = useId();
167
- const asideShown = Boolean(renderAside) && asideOpen;
269
+ const asideShown = Boolean(renderAside) && asideOpen && !artifactShown;
168
270
  const closeAside = () => setAsideOpen(false);
169
- const toggleAside = () => setAsideOpen((current) => !current);
271
+ const toggleAside = () => {
272
+ if (artifactShown) {
273
+ closeArtifactPanel();
274
+ setAsideOpen(true);
275
+ return;
276
+ }
277
+ setAsideOpen((current) => !current);
278
+ };
170
279
  const startersNode = isEmpty && showStarters && starters.length > 0 && /* @__PURE__ */ jsx("div", { className: "ds-chat-widget__starters", children: /* @__PURE__ */ jsx(
171
280
  PromptSuggestions,
172
281
  {
@@ -182,8 +291,12 @@ function ChatWidget({
182
291
  }
183
292
  ) });
184
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);
185
298
  const headerActions = /* @__PURE__ */ jsxs(Fragment, { children: [
186
- !(renderThreads && threadsWide) && /* @__PURE__ */ jsx(
299
+ showNewChat && /* @__PURE__ */ jsx(
187
300
  CircularButton,
188
301
  {
189
302
  icon: "edit_square",
@@ -210,6 +323,23 @@ function ChatWidget({
210
323
  onClick: toggleAside
211
324
  }
212
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
+ ),
213
343
  placementEnabled && /* @__PURE__ */ jsx(
214
344
  "span",
215
345
  {
@@ -292,11 +422,12 @@ function ChatWidget({
292
422
  "data-widgets-wide": widgetsNode && widgetsWidth === "wide" ? "" : void 0,
293
423
  "data-scrolling": widgetsNode && widgetsLayout === "scroll" && rootScrolling || void 0,
294
424
  onScroll: handleRootScroll,
295
- "data-lifted-actions": asideShown && asideWide || void 0,
425
+ "data-lifted-actions": trailingInline || void 0,
296
426
  onKeyDown: (e) => {
297
- if (e.key === "Escape" && (threadsOpen || asideShown && !asideWide)) {
427
+ if (e.key === "Escape" && (threadsOpen || asideShown && !asideWide || artifactShown && !asideWide)) {
298
428
  e.stopPropagation();
299
429
  if (threadsOpen) closeThreads();
430
+ else if (artifactShown) closeArtifactPanel();
300
431
  else closeAside();
301
432
  return;
302
433
  }
@@ -318,7 +449,7 @@ function ChatWidget({
318
449
  ChatHeader,
319
450
  {
320
451
  title: /* @__PURE__ */ jsxs("span", { className: "ds-chat-widget__brand", children: [
321
- renderThreads && !threadsWide && /* @__PURE__ */ jsx(
452
+ renderThreads && !threadsInline && /* @__PURE__ */ jsx(
322
453
  CircularButton,
323
454
  {
324
455
  icon: "menu",
@@ -334,13 +465,20 @@ function ChatWidget({
334
465
  actions: (
335
466
  /* With the agent rail seated inline the cluster moves to the
336
467
  widget's own right edge (see headerActions). */
337
- asideShown && asideWide ? void 0 : 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
+ )
338
476
  )
339
477
  }
340
478
  ),
341
479
  tabsNode && /* @__PURE__ */ jsx("div", { className: "ds-chat-widget__tabs-row", children: tabsNode }),
342
480
  /* @__PURE__ */ jsxs("div", { className: "ds-chat-widget__body", children: [
343
- /* @__PURE__ */ jsx(ChatThread, { className: "ds-chat-widget__thread", children: [
481
+ /* @__PURE__ */ jsx(ArtifactSeatContext.Provider, { value: artifacts, children: /* @__PURE__ */ jsx(ChatThread, { className: "ds-chat-widget__thread", children: [
344
482
  ...turns.map(
345
483
  (turn) => turn.bare ? (
346
484
  /* Conversation furniture (a day marker): the injected
@@ -364,7 +502,7 @@ function ChatWidget({
364
502
  ) : /* @__PURE__ */ jsx(AssistantTurn, { turn }, turn.id)
365
503
  ),
366
504
  ...live ? [/* @__PURE__ */ jsx(AssistantTurn, { live }, live.id)] : []
367
- ] }),
505
+ ] }) }),
368
506
  isEmpty && /* @__PURE__ */ jsx("div", { className: "ds-chat-widget__welcome", children: /* @__PURE__ */ jsxs("div", { className: "ds-chat-widget__greeting", children: [
369
507
  /* @__PURE__ */ jsx("p", { className: "ds-chat-widget__hello", children: greeting }),
370
508
  /* @__PURE__ */ jsx("p", { className: "ds-chat-widget__ask", children: tagline })
@@ -434,61 +572,137 @@ function ChatWidget({
434
572
  }
435
573
  );
436
574
  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
- ] });
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
+ );
492
706
  }
493
707
  export {
494
708
  ChatWidget
@@ -2,7 +2,9 @@ import { ChatCard } from './types';
2
2
  /**
3
3
  * A turn's `card`, drawn through the provider's renderers. A kind with no
4
4
  * renderer draws nothing: a card is an enrichment of an answer that already
5
- * stands in text, so an unknown kind degrades to the words alone.
5
+ * stands in text, so an unknown kind degrades to the words alone. The one
6
+ * built-in kind is the artifact, drawn as its tile unless the host has
7
+ * taken the kind over with a renderer of its own.
6
8
  */
7
9
  export declare function TurnCard({ card }: {
8
10
  card: ChatCard;
@@ -1,10 +1,17 @@
1
1
  "use client";
2
2
  import { jsx, Fragment } from "react/jsx-runtime";
3
- import { useChatWidget } from "./context.js";
3
+ import { useContext } from "react";
4
+ import { cardArtifact } from "./artifacts.js";
5
+ import { ChatArtifactCard } from "./ChatArtifactCard.js";
6
+ import { useChatWidget, ArtifactSeatContext } from "./context.js";
4
7
  function TurnCard({ card }) {
5
8
  const { renderers } = useChatWidget();
9
+ const artifactSeat = useContext(ArtifactSeatContext);
6
10
  const render = renderers[card.kind];
7
- if (!render) return null;
11
+ if (!render) {
12
+ const artifact = artifactSeat ? cardArtifact(card) : null;
13
+ return artifact ? /* @__PURE__ */ jsx(ChatArtifactCard, { artifact }) : null;
14
+ }
8
15
  return /* @__PURE__ */ jsx(Fragment, { children: render(card.data) });
9
16
  }
10
17
  export {
@@ -0,0 +1,5 @@
1
+ import { ChatArtifact, ChatCard } from './types';
2
+ /** The artifact a card carries, or null when it is some other kind of card. */
3
+ export declare const cardArtifact: (card: ChatCard | undefined) => ChatArtifact | null;
4
+ /** Wraps an artifact as the card a transport sends. */
5
+ export declare const artifactCard: (artifact: ChatArtifact) => ChatCard;
@@ -0,0 +1,14 @@
1
+ import { ARTIFACT_CARD_KIND } from "./types.js";
2
+ const isArtifact = (data) => {
3
+ const candidate = data;
4
+ return typeof candidate === "object" && candidate !== null && typeof candidate.id === "string" && typeof candidate.title === "string" && typeof candidate.kind === "string";
5
+ };
6
+ const cardArtifact = (card) => card?.kind === ARTIFACT_CARD_KIND && isArtifact(card.data) ? card.data : null;
7
+ const artifactCard = (artifact) => ({
8
+ kind: ARTIFACT_CARD_KIND,
9
+ data: artifact
10
+ });
11
+ export {
12
+ artifactCard,
13
+ cardArtifact
14
+ };
package/chat/context.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { ReactNode } from 'react';
2
2
  import { ChatPlacement, ChatSide } from './placement';
3
3
  import { ThreadsState } from './threads';
4
- import { ChatFeedbackVerdict } from './types';
4
+ import { ChatArtifact, ChatFeedbackVerdict } from './types';
5
5
  import { ChatState } from './useChat';
6
6
  /** Panel is the docked rail; full is the viewport takeover. */
7
7
  export type ChatView = 'panel' | 'full';
@@ -10,6 +10,17 @@ export type ChatView = 'panel' | 'full';
10
10
  * a kind with no renderer draws nothing.
11
11
  */
12
12
  export type ChatCardRenderers = Record<string, (data: unknown) => ReactNode>;
13
+ /**
14
+ * Draws an artifact's preview as a React node, keyed by the artifact's
15
+ * `kind`. A kind with no renderer falls back to the built-in frames.
16
+ */
17
+ export type ChatArtifactRenderers = Record<string, (artifact: ChatArtifact) => ReactNode>;
18
+ /**
19
+ * Whether the widget rendering this subtree seats the artifact panel (its
20
+ * `artifacts` prop). With no seat an artifact has nowhere to open, so the
21
+ * thread draws no tile for it: the feature is whole or it is absent.
22
+ */
23
+ export declare const ArtifactSeatContext: import('react').Context<boolean>;
13
24
  /** How the widget renders a link the model wrote. */
14
25
  export type ChatLinkRenderer = (props: {
15
26
  href: string;
@@ -52,6 +63,12 @@ export interface ChatWidgetContextValue extends ChatState {
52
63
  /** The thread lifecycle behind the built-in threads and tabs. */
53
64
  threads: ThreadsState;
54
65
  renderers: ChatCardRenderers;
66
+ /** The artifact on stage in the artifact panel, or null while it is closed. */
67
+ artifact: ChatArtifact | null;
68
+ /** Puts an artifact on stage. The widget seats the panel; this only says which. */
69
+ openArtifact: (artifact: ChatArtifact) => void;
70
+ closeArtifact: () => void;
71
+ artifactRenderers: ChatArtifactRenderers;
55
72
  renderLink: ChatLinkRenderer;
56
73
  isInternalHref: (href: string) => boolean;
57
74
  /** Record and send a thumbs verdict; null when feedback is off. */
package/chat/context.js CHANGED
@@ -1,5 +1,6 @@
1
1
  "use client";
2
2
  import { useContext, createContext } from "react";
3
+ const ArtifactSeatContext = createContext(false);
3
4
  const ChatWidgetContext = createContext(null);
4
5
  function useChatWidget() {
5
6
  const context = useContext(ChatWidgetContext);
@@ -20,6 +21,7 @@ function createFeedbackPoster(endpoint = "/api/chat/feedback") {
20
21
  };
21
22
  }
22
23
  export {
24
+ ArtifactSeatContext,
23
25
  ChatWidgetContext,
24
26
  createFeedbackPoster,
25
27
  isInternalPath,
@@ -0,0 +1,6 @@
1
+ import { ChatArtifactRenderers } from './context';
2
+ import { ChatArtifact } from './types';
3
+ /** Draws the demo's live artifact kinds. Pass to `ChatProvider`'s `artifactRenderers`. */
4
+ export declare const DEMO_ARTIFACT_RENDERERS: ChatArtifactRenderers;
5
+ export declare const DEMO_DASHBOARD_ARTIFACT: ChatArtifact;
6
+ export declare const DEMO_PAGE_ARTIFACT: ChatArtifact;