rift-ds 1.9.0 → 1.10.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 (79) hide show
  1. package/README.md +3 -3
  2. package/bin/templates/chat/app/api/chat/route.ts +3 -0
  3. package/chat/AssistantTurn.js +5 -2
  4. package/chat/ChatWidget.css +5 -4
  5. package/chat/ChatWidget.d.ts +10 -1
  6. package/chat/ChatWidget.js +17 -4
  7. package/chat/DemoWelcome.js +17 -5
  8. package/chat/FollowupSuggestions.d.ts +7 -4
  9. package/chat/FollowupSuggestions.js +5 -3
  10. package/chat/TurnQuestion.d.ts +19 -0
  11. package/chat/TurnQuestion.js +38 -0
  12. package/chat/demo-script.js +256 -50
  13. package/chat/followups.d.ts +2 -2
  14. package/chat/followups.js +1 -0
  15. package/chat/index.d.ts +2 -1
  16. package/chat/index.js +11 -0
  17. package/chat/queries.d.ts +2 -2
  18. package/chat/queries.js +3 -2
  19. package/chat/questions.d.ts +97 -0
  20. package/chat/questions.js +140 -0
  21. package/chat/registry.json.d.ts +3 -2
  22. package/chat/registry.json.js +1 -1
  23. package/chat/server.d.ts +19 -0
  24. package/chat/server.js +45 -1
  25. package/chat/sim.d.ts +12 -1
  26. package/chat/sim.js +19 -3
  27. package/chat/types.d.ts +58 -1
  28. package/chat/types.js +1 -0
  29. package/chat/useChat.d.ts +16 -1
  30. package/chat/useChat.js +39 -2
  31. package/components/AlertDialog/AlertDialog.css +1 -1
  32. package/components/AnchorNav/AnchorNav.css +1 -1
  33. package/components/AppSidebar/AppSidebar.css +5 -5
  34. package/components/AppSidebar/AppSidebar.js +3 -2
  35. package/components/Attachment/Attachment.css +3 -1
  36. package/components/BottomNav/BottomNav.css +1 -1
  37. package/components/Chart/Chart.css +1 -0
  38. package/components/ChatThread/ChatThread.css +21 -0
  39. package/components/ChatThread/ChatThread.d.ts +12 -1
  40. package/components/ChatThread/ChatThread.js +40 -1
  41. package/components/ColorPicker/ColorPicker.css +1 -1
  42. package/components/Combobox/Combobox.css +1 -1
  43. package/components/CommandPalette/CommandPalette.css +1 -1
  44. package/components/Composer/Composer.css +1 -1
  45. package/components/ContextMenu/ContextMenu.css +1 -1
  46. package/components/Dialog/Dialog.css +1 -1
  47. package/components/Drawer/Drawer.css +1 -1
  48. package/components/Dropdown/Dropdown.css +1 -1
  49. package/components/DropdownMenu/DropdownMenu.css +1 -1
  50. package/components/FilterBar/FilterBar.css +1 -1
  51. package/components/HoverCard/HoverCard.css +1 -1
  52. package/components/Lightbox/Lightbox.css +1 -1
  53. package/components/ModelPicker/ModelPicker.css +1 -1
  54. package/components/Popover/Popover.css +1 -1
  55. package/components/PromptSuggestions/PromptSuggestions.css +67 -0
  56. package/components/PromptSuggestions/PromptSuggestions.d.ts +8 -0
  57. package/components/PromptSuggestions/PromptSuggestions.js +34 -26
  58. package/components/QuestionCard/QuestionCard.css +358 -0
  59. package/components/QuestionCard/QuestionCard.d.ts +136 -0
  60. package/components/QuestionCard/QuestionCard.js +371 -0
  61. package/components/RichDropdown/RichDropdown.css +1 -1
  62. package/components/SourceChip/SourceChip.css +1 -1
  63. package/components/TimePicker/TimePicker.css +1 -1
  64. package/components/Toast/Toast.css +1 -1
  65. package/components/Tooltip/Tooltip.css +1 -1
  66. package/components/TopAppBar/TopAppBar.css +1 -1
  67. package/components/registry.json +8 -0
  68. package/components/registry.json.d.ts +8 -0
  69. package/components/registry.json.js +1 -1
  70. package/index.d.ts +1 -0
  71. package/index.js +3 -0
  72. package/package.json +5 -1
  73. package/tokens/breakpoints.d.ts +60 -0
  74. package/tokens/breakpoints.js +92 -0
  75. package/tokens/registry.d.ts +1 -1
  76. package/tokens/registry.json +14 -0
  77. package/tokens/registry.json.d.ts +14 -0
  78. package/tokens/registry.json.js +1 -1
  79. package/tokens/tokens-light.css +22 -0
package/README.md CHANGED
@@ -13,7 +13,7 @@ An open source React design system built for AI products and coding agents: comp
13
13
 
14
14
  ## Why Rift DS
15
15
 
16
- - **<!-- component-count -->152<!-- /component-count --> components.** Buttons, forms and tables through to charts, navigation and overlays, each typed and documented with live examples. React is the only required peer dependency.
16
+ - **<!-- component-count -->153<!-- /component-count --> components.** Buttons, forms and tables through to charts, navigation and overlays, each typed and documented with live examples. React is the only required peer dependency.
17
17
  - **AI patterns, the whole chat and templates, ready to use.** Composer, streaming chat thread, tool calls, reasoning and thread panel are in the box, the complete AI chat ships assembled as one widget that one command scaffolds, and the docs site has complete template screens built from them.
18
18
  - **Fully themeable.** The shipped themes are complete looks to start from, each in light and dark. One `data-brand` attribute applies a theme, and a few CSS overrides make your own.
19
19
  - **Ready for coding agents.** One command installs the agent skills, and a public MCP endpoint serves every component's props, so your agent builds with the library as it actually is. No key, no account.
@@ -94,13 +94,13 @@ import 'rift-ds/tokens/presets/presets.css';
94
94
 
95
95
  ## Components
96
96
 
97
- <!-- component-count -->152<!-- /component-count --> components, including a chat set for AI products (Chat thread, Composer, Tool call, Reasoning, Thread panel) and Shader field, a WebGL2 ambient background whose light sources read your colour tokens at runtime, so it re-themes with everything else. Details and live examples are on the [docs site](https://rift-ds.com/components).
97
+ <!-- component-count -->153<!-- /component-count --> components, including a chat set for AI products (Chat thread, Composer, Tool call, Reasoning, Thread panel) and Shader field, a WebGL2 ambient background whose light sources read your colour tokens at runtime, so it re-themes with everything else. Details and live examples are on the [docs site](https://rift-ds.com/components).
98
98
 
99
99
  <details>
100
100
  <summary>The full list</summary>
101
101
 
102
102
  <!-- component-list:start -->
103
- Accordion · Agent plan · Agent rail · Agent status · AI button · Alert · Alert dialog · Anchor nav · Animated number · App layout · App sidebar · Area chart · Artifact card · Artifact panel · Attachment drop zone · Attachment group · Attachment tile · Attachment viewer · Avatar · Avatar group · Badge · Banner · Bar chart · Bottom nav · Breadcrumb · Button · Button group · Card · Card stack · Carousel · Chat header · Chat marker · Chat message · Chat thread · Checkbox · Chip · Circular button · Code block · Code diff · Colour picker · Combo chart · Combobox · Command palette · Composer · Contact card · Context menu · Contribution graph · Data table · Date input · Date picker · Dialog · Divider · Document chip · Drawer · Dropdown · Dropdown menu · Empty state · Entity card · Event calendar · Field · Figure · File input · Filter bar · Funnel chart · Gantt chart · Gauge · Globe · Hover card · Image compare · Input · Inspector colour picker · Inspector dropdown · Inspector input · Inspector section · Inspector segments · Inspector slider · Inspector toggle switch · Instructions · Interrupt card · Kbd · Legend tile · Lightbox · Line chart · Link list · Map callout · Map legend · Mention · Message actions · Message card · Meter · Model picker · Nav · Nav list · Notification centre · Number input · Pagination · Panel · Pie chart · Pin input · Pixel avatar · Popover · Progress bar · Prompt suggestions · Prose · Quote · Radar chart · Radial chart · Radio button · Rating · Reasoning · Rich dropdown · Scatter chart · Section title · Segmented control · Selection card · Shader field · Sidebar panel · Skeleton · Slider · Source chip · Source trail · Sparkline · Spinner · Split button · Split pane · Stacked bar chart · Stat · Status dot · Stepper · Streaming text · Swatch · Table · Tabs · Tag input · Textarea · Thread panel · Thread tabs · Time picker · Timeline · Toast · Toggle group · Toggle switch · Tool call · Toolbar · Tooltip · Top app bar · Tree view · Treemap · Usage card · Waveform · Widget · World map
103
+ Accordion · Agent plan · Agent rail · Agent status · AI button · Alert · Alert dialog · Anchor nav · Animated number · App layout · App sidebar · Area chart · Artifact card · Artifact panel · Attachment drop zone · Attachment group · Attachment tile · Attachment viewer · Avatar · Avatar group · Badge · Banner · Bar chart · Bottom nav · Breadcrumb · Button · Button group · Card · Card stack · Carousel · Chat header · Chat marker · Chat message · Chat thread · Checkbox · Chip · Circular button · Code block · Code diff · Colour picker · Combo chart · Combobox · Command palette · Composer · Contact card · Context menu · Contribution graph · Data table · Date input · Date picker · Dialog · Divider · Document chip · Drawer · Dropdown · Dropdown menu · Empty state · Entity card · Event calendar · Field · Figure · File input · Filter bar · Funnel chart · Gantt chart · Gauge · Globe · Hover card · Image compare · Input · Inspector colour picker · Inspector dropdown · Inspector input · Inspector section · Inspector segments · Inspector slider · Inspector toggle switch · Instructions · Interrupt card · Kbd · Legend tile · Lightbox · Line chart · Link list · Map callout · Map legend · Mention · Message actions · Message card · Meter · Model picker · Nav · Nav list · Notification centre · Number input · Pagination · Panel · Pie chart · Pin input · Pixel avatar · Popover · Progress bar · Prompt suggestions · Prose · Question card · Quote · Radar chart · Radial chart · Radio button · Rating · Reasoning · Rich dropdown · Scatter chart · Section title · Segmented control · Selection card · Shader field · Sidebar panel · Skeleton · Slider · Source chip · Source trail · Sparkline · Spinner · Split button · Split pane · Stacked bar chart · Stat · Status dot · Stepper · Streaming text · Swatch · Table · Tabs · Tag input · Textarea · Thread panel · Thread tabs · Time picker · Timeline · Toast · Toggle group · Toggle switch · Tool call · Toolbar · Tooltip · Top app bar · Tree view · Treemap · Usage card · Waveform · Widget · World map
104
104
  <!-- component-list:end -->
105
105
 
106
106
  </details>
@@ -23,6 +23,9 @@ const handler = createChatHandler({
23
23
  models: CHAT_MODELS,
24
24
  defaultModel: DEFAULT_CHAT_MODEL,
25
25
  // tools: [], // Deterministic lookups the model may call; a tool can answer with a card.
26
+ // A reply can end in a question with answers to pick, drawn as a card the
27
+ // visitor taps. Asking ends the exchange; the answer arrives as the next message.
28
+ askUser: true,
26
29
  guardrails,
27
30
  });
28
31
 
@@ -12,6 +12,7 @@ import { DOCK_QUERY } from "./queries.js";
12
12
  import { FollowupSuggestions } from "./FollowupSuggestions.js";
13
13
  import { ResponseActions } from "./ResponseActions.js";
14
14
  import { TurnCard } from "./TurnContent.js";
15
+ import { TurnQuestion } from "./TurnQuestion.js";
15
16
  function MarkdownLink({ href, children }) {
16
17
  const { view, setOpen, renderLink, isInternalHref } = useChatWidget();
17
18
  if (!href || !isInternalHref(href)) return /* @__PURE__ */ jsx(Fragment, { children });
@@ -44,7 +45,7 @@ function AssistantTurn({ turn, live }) {
44
45
  const durationSeconds = live ? live.durationSeconds : turn?.durationSeconds;
45
46
  const text = live ? live.text : turn?.text ?? "";
46
47
  const { turns, live: liveTurn, freshFollowupsId } = useChatWidget();
47
- const followupsPending = !!turn && turn.text !== "" && !!turn.exchangeId && !turn.followups && !liveTurn && turns[turns.length - 1]?.id === turn.id;
48
+ const followupsPending = !!turn && turn.text !== "" && !!turn.exchangeId && !turn.followups && !turn.question && !liveTurn && turns[turns.length - 1]?.id === turn.id;
48
49
  return /* @__PURE__ */ jsx(
49
50
  ChatMessage,
50
51
  {
@@ -56,6 +57,7 @@ function AssistantTurn({ turn, live }) {
56
57
  {
57
58
  turnId: turn.id,
58
59
  suggestions: turn.followups,
60
+ chosen: turn.followupChosen,
59
61
  entrance: turn.id === freshFollowupsId
60
62
  }
61
63
  ) : followupsPending && turn ? /* @__PURE__ */ jsx(FollowupSuggestions, { turnId: turn.id, suggestions: [], pending: true }) : void 0,
@@ -72,7 +74,8 @@ function AssistantTurn({ turn, live }) {
72
74
  ),
73
75
  turn?.content,
74
76
  turn?.card && /* @__PURE__ */ jsx(TurnCard, { card: turn.card }),
75
- text !== "" && /* @__PURE__ */ jsx(Prose, { children: /* @__PURE__ */ jsx(ReactMarkdown, { remarkPlugins: [remarkGfm], components: markdownComponents, children: text }) })
77
+ text !== "" && /* @__PURE__ */ jsx(Prose, { children: /* @__PURE__ */ jsx(ReactMarkdown, { remarkPlugins: [remarkGfm], components: markdownComponents, children: text }) }),
78
+ turn?.question && /* @__PURE__ */ jsx(TurnQuestion, { turn })
76
79
  ] })
77
80
  }
78
81
  );
@@ -976,7 +976,7 @@
976
976
  box-shadow: var(--shadow-floating);
977
977
  overflow: hidden;
978
978
  backdrop-filter: blur(24px);
979
- z-index: 1100;
979
+ z-index: var(--layer-chat-panel);
980
980
  /* The panel-to-takeover glide: the same card grows to fill the viewport,
981
981
  so every property that differs between the two views animates together. */
982
982
  transition:
@@ -1050,7 +1050,7 @@ html[data-chat-resizing] .ds-chat-launcher__panel {
1050
1050
  position: fixed;
1051
1051
  inset: 0;
1052
1052
  background: var(--color-scrim);
1053
- z-index: 1090;
1053
+ z-index: var(--layer-chat-scrim);
1054
1054
  animation: ds-chat-launcher-scrim-in var(--motion-duration-slow) var(--motion-ease-standard);
1055
1055
  }
1056
1056
 
@@ -1075,7 +1075,7 @@ html[data-chat-resizing] .ds-chat-launcher__panel {
1075
1075
  justify-content: center;
1076
1076
  cursor: ew-resize;
1077
1077
  touch-action: none;
1078
- z-index: 1101;
1078
+ z-index: calc(var(--layer-chat-panel) + 1);
1079
1079
  }
1080
1080
 
1081
1081
  .ds-chat-launcher__dock-handle--left {
@@ -1135,6 +1135,7 @@ html[data-chat-resizing] .ds-chat-launcher__panel {
1135
1135
  corner, all inside the card. */
1136
1136
  .ds-chat-launcher__float-grip {
1137
1137
  position: absolute;
1138
+ /* ds-allow(stacking): local order inside the floating card, above its own content; the card itself sits on --layer-chat-panel. */
1138
1139
  z-index: 10;
1139
1140
  display: flex;
1140
1141
  align-items: center;
@@ -1246,7 +1247,7 @@ html[data-chat-resizing] .ds-chat-launcher__panel--floating .ds-chat-header {
1246
1247
  position: fixed;
1247
1248
  right: var(--ds-chat-inset);
1248
1249
  bottom: var(--ds-chat-inset);
1249
- z-index: 1080;
1250
+ z-index: var(--layer-chat-launcher);
1250
1251
  }
1251
1252
 
1252
1253
  /* The button's own solid field keeps it legible over whatever content
@@ -37,8 +37,17 @@ export interface ChatWidgetProps {
37
37
  phone?: boolean;
38
38
  /** The header brand name. */
39
39
  title?: string;
40
+ /**
41
+ * Which way the conversation reads. `top-down` (the default) floats each
42
+ * new turn to the top of the thread and streams the answer in below it.
43
+ * `bottom-up` is the classic message thread: turns stack from the bottom,
44
+ * the newest just above the composer, and the view follows the answer.
45
+ */
46
+ flow?: 'top-down' | 'bottom-up';
40
47
  /** The composer's placeholder text. */
41
48
  placeholder?: string;
49
+ /** The composer's placeholder while a set of questions holds it: the answer goes in the card, or the card is skipped. */
50
+ questionsPlaceholder?: string;
42
51
  /** Show the conversation starters on the welcome screen. */
43
52
  showStarters?: boolean;
44
53
  /** The header mark's image src; null (the default) shows the name alone. */
@@ -112,4 +121,4 @@ export interface ChatWidgetProps {
112
121
  * a stage card, a whole page). It reads all of its state from ChatProvider,
113
122
  * so the conversation is the same object wherever it shows.
114
123
  */
115
- 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, artifacts, contextLabel, widgets, widgetsLayout, widgetsWidth, disclaimer, }: ChatWidgetProps): import("react").JSX.Element;
124
+ export declare function ChatWidget({ fullscreenEnabled, closeEnabled, placementEnabled, compact, phone, title, flow, placeholder, questionsPlaceholder, showStarters, logo, tagline, starters: startersInput, startersKey, composerActions, models, defaultModel, lockedModelDescription, fileAttachments, maxAttachments, maxAttachmentBytes, mentions, threads, threadsProps, tabs, aside, agent, artifacts, contextLabel, widgets, widgetsLayout, widgetsWidth, disclaimer, }: ChatWidgetProps): import("react").JSX.Element;
@@ -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, useMemo } from "react";
3
+ import { useRef, useEffect, useSyncExternalStore, 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";
@@ -27,6 +27,8 @@ import { CHAT_PLACEMENTS, chatPlacement } from "./placement.js";
27
27
  import { ASIDE_WITH_THREADS_MIN_WIDTH, ARTIFACT_MIN_COLUMN_WIDTH, THREADS_RAIL_MIN_WIDTH, ASIDE_RAIL_MIN_WIDTH } from "./queries.js";
28
28
  import { fitsChip } from "./suggestions.js";
29
29
  import { TurnCard } from "./TurnContent.js";
30
+ import { isBlockingQuestion } from "./questions.js";
31
+ import { ReplyBody } from "./TurnQuestion.js";
30
32
  import "./ChatWidget.css";
31
33
  const DEFAULT_MAX_ATTACHMENTS = 10;
32
34
  const DEFAULT_MAX_ATTACHMENT_BYTES = 50 * 1024 * 1024;
@@ -40,7 +42,9 @@ function ChatWidget({
40
42
  compact = false,
41
43
  phone = false,
42
44
  title = "Assistant",
45
+ flow = "top-down",
43
46
  placeholder = "Ask anything",
47
+ questionsPlaceholder = "Answer the questions above, or skip them",
44
48
  showStarters = true,
45
49
  logo = null,
46
50
  tagline = "How can I help you today?",
@@ -91,6 +95,14 @@ function ChatWidget({
91
95
  openArtifact,
92
96
  closeArtifact
93
97
  } = useChatWidget();
98
+ const questionsOpen = isBlockingQuestion(turns[turns.length - 1], live !== null);
99
+ const questionsWereOpen = useRef(questionsOpen);
100
+ useEffect(() => {
101
+ if (questionsWereOpen.current && !questionsOpen) {
102
+ composerRef.current?.focus({ preventScroll: true });
103
+ }
104
+ questionsWereOpen.current = questionsOpen;
105
+ }, [questionsOpen]);
94
106
  const lockedValues = modelPolicy?.locked ?? [];
95
107
  const pick = chosenModel && !lockedValues.includes(chosenModel) ? chosenModel : null;
96
108
  const pickerValue = pick ?? modelPolicy?.default ?? defaultModel ?? models?.[0]?.value;
@@ -478,7 +490,7 @@ function ChatWidget({
478
490
  ),
479
491
  tabsNode && /* @__PURE__ */ jsx("div", { className: "ds-chat-widget__tabs-row", children: tabsNode }),
480
492
  /* @__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: [
493
+ /* @__PURE__ */ jsx(ArtifactSeatContext.Provider, { value: artifacts, children: /* @__PURE__ */ jsx(ChatThread, { className: "ds-chat-widget__thread", flow, children: [
482
494
  ...turns.map(
483
495
  (turn) => turn.bare ? (
484
496
  /* Conversation furniture (a day marker): the injected
@@ -495,7 +507,7 @@ function ChatWidget({
495
507
  turn.content,
496
508
  turn.card && /* @__PURE__ */ jsx(TurnCard, { card: turn.card })
497
509
  ] }),
498
- children: mentions && turn.text ? /* @__PURE__ */ jsx(MentionText, { text: turn.text, sources: mentions }) : turn.text
510
+ children: turn.reply ? /* @__PURE__ */ jsx(ReplyBody, { reply: turn.reply, text: turn.text }) : mentions && turn.text ? /* @__PURE__ */ jsx(MentionText, { text: turn.text, sources: mentions }) : turn.text
499
511
  },
500
512
  turn.id
501
513
  )
@@ -515,7 +527,8 @@ function ChatWidget({
515
527
  Composer,
516
528
  {
517
529
  ref: composerRef,
518
- placeholder,
530
+ placeholder: questionsOpen ? questionsPlaceholder : placeholder,
531
+ disabled: questionsOpen,
519
532
  sendLabel: "Send",
520
533
  aiGlow: true,
521
534
  value: draft,
@@ -61,7 +61,7 @@ function DemoBoard({ onAsk }) {
61
61
  /* @__PURE__ */ jsx(WidgetRow, { title: "How is the trial going?", meta: "Ask", onClick: () => onAsk("How is the trial going?") })
62
62
  ] }),
63
63
  /* @__PURE__ */ jsx(Widget, { title: "Projects", count: BOARD_PROJECTS.length, children: BOARD_PROJECTS.map(([name, state, when]) => /* @__PURE__ */ jsx(WidgetRow, { leading: /* @__PURE__ */ jsx(PixelAvatar, { name }), title: name, description: state, meta: when }, name)) }),
64
- /* @__PURE__ */ jsxs(Widget, { title: "To do", count: 4, children: [
64
+ /* @__PURE__ */ jsxs(Widget, { title: "To do", count: 5, children: [
65
65
  /* @__PURE__ */ jsxs(WidgetGroup, { label: "Waiting on you", children: [
66
66
  /* @__PURE__ */ jsx(
67
67
  WidgetRow,
@@ -70,7 +70,7 @@ function DemoBoard({ onAsk }) {
70
70
  title: "Invite the other three",
71
71
  description: "2 of 5 seats filled",
72
72
  meta: "Today",
73
- onClick: () => onAsk("Go ahead and invite the other three")
73
+ onClick: () => onAsk("Invite my team to a workspace")
74
74
  }
75
75
  ),
76
76
  /* @__PURE__ */ jsx(
@@ -80,7 +80,17 @@ function DemoBoard({ onAsk }) {
80
80
  title: "Choose a plan",
81
81
  description: "Trial ends in 9 days",
82
82
  meta: "Oct 19",
83
- onClick: () => onAsk("Which plan fits a team of five?")
83
+ onClick: () => onAsk("Help me pick a plan")
84
+ }
85
+ ),
86
+ /* @__PURE__ */ jsx(
87
+ WidgetRow,
88
+ {
89
+ leading: /* @__PURE__ */ jsx(StatusDot, { variant: "info", size: "sm", "aria-hidden": "true" }),
90
+ title: "Plan the launch kickoff",
91
+ description: "A date, who comes, and how",
92
+ meta: "Oct 21",
93
+ onClick: () => onAsk("Plan our launch kickoff")
84
94
  }
85
95
  )
86
96
  ] }),
@@ -91,7 +101,8 @@ function DemoBoard({ onAsk }) {
91
101
  leading: /* @__PURE__ */ jsx(StatusDot, { variant: "positive", size: "sm", "aria-hidden": "true" }),
92
102
  title: "Shared calendar",
93
103
  description: "Synced an hour ago",
94
- meta: "On"
104
+ meta: "On",
105
+ onClick: () => onAsk("Find a time we can all meet")
95
106
  }
96
107
  ),
97
108
  /* @__PURE__ */ jsx(
@@ -100,7 +111,8 @@ function DemoBoard({ onAsk }) {
100
111
  leading: /* @__PURE__ */ jsx(StatusDot, { variant: "neutral", size: "sm", "aria-hidden": "true" }),
101
112
  title: "Mention notifications",
102
113
  description: "Quiet hours, 6pm to 8am",
103
- meta: "Paused"
114
+ meta: "Paused",
115
+ onClick: () => onAsk("Set up notifications for me")
104
116
  }
105
117
  )
106
118
  ] })
@@ -4,15 +4,16 @@
4
4
  * one pattern for "here is something you could ask", whether the conversation
5
5
  * has started or not. The only difference is where the questions come from.
6
6
  *
7
- * Tapping one sends it as the next message. A tap while a response is already
8
- * streaming is ignored by `send`, the same as pressing enter in the composer
9
- * would be.
7
+ * Tapping one sends it as the next message and keeps the row as the record
8
+ * of that choice: the chip checked, the rest dimmed and inert. A tap while a
9
+ * response is already streaming is ignored by `send`, the same as pressing
10
+ * enter in the composer would be, and leaves the row as it was.
10
11
  *
11
12
  * While the questions are still being generated, `pending` shows the row's
12
13
  * shimmer placeholders in their place: a real loading state, unlike the
13
14
  * welcome starters' staged one.
14
15
  */
15
- export declare function FollowupSuggestions({ turnId, suggestions, pending, entrance, }: {
16
+ export declare function FollowupSuggestions({ turnId, suggestions, pending, entrance, chosen, }: {
16
17
  /** The turn these belong to; makes each suggestion's id unique in the thread. */
17
18
  turnId: string;
18
19
  suggestions: string[];
@@ -20,4 +21,6 @@ export declare function FollowupSuggestions({ turnId, suggestions, pending, entr
20
21
  pending?: boolean;
21
22
  /** Play the chips' staggered pop-in. Off for rows remounted by a reopen, whose suggestions already arrived once. */
22
23
  entrance?: boolean;
24
+ /** The label already sent from this row, which keeps the row as a record. */
25
+ chosen?: string;
23
26
  }): import("react").JSX.Element;
@@ -6,9 +6,10 @@ function FollowupSuggestions({
6
6
  turnId,
7
7
  suggestions,
8
8
  pending = false,
9
- entrance = true
9
+ entrance = true,
10
+ chosen
10
11
  }) {
11
- const { send } = useChatWidget();
12
+ const { chooseFollowup } = useChatWidget();
12
13
  const items = suggestions.map((label, index) => ({
13
14
  id: `${turnId}-followup-${index}`,
14
15
  label
@@ -22,9 +23,10 @@ function FollowupSuggestions({
22
23
  pending,
23
24
  entrance,
24
25
  suggestions: items,
26
+ value: chosen !== void 0 ? items.find((item) => item.label === chosen)?.id : void 0,
25
27
  onValueChange: (id) => {
26
28
  const item = items.find((candidate) => candidate.id === id);
27
- if (item) send(item.label);
29
+ if (item) chooseFollowup(turnId, item.label);
28
30
  }
29
31
  }
30
32
  );
@@ -0,0 +1,19 @@
1
+ import { ChatReply, ChatTurn } from './types';
2
+ /**
3
+ * The questions an assistant turn asks, drawn under its prose. Live only
4
+ * while the turn is the newest and nothing is streaming: once the
5
+ * conversation moves on without an answer the card stays as it was offered,
6
+ * inert, so the transcript still shows what was asked.
7
+ */
8
+ export declare function TurnQuestion({ turn }: {
9
+ turn: ChatTurn;
10
+ }): import("react").JSX.Element | null;
11
+ /**
12
+ * A user turn that answered a set of questions: each question over its
13
+ * answers. One question, with one pick or several, stays the ordinary
14
+ * bubble, its picks joined with commas.
15
+ */
16
+ export declare function ReplyBody({ reply, text }: {
17
+ reply?: ChatReply;
18
+ text: string;
19
+ }): import("react").JSX.Element;
@@ -0,0 +1,38 @@
1
+ "use client";
2
+ import { jsx, Fragment } from "react/jsx-runtime";
3
+ import { useRef, useEffect } from "react";
4
+ import { QuestionSummary, QuestionCard } from "../components/QuestionCard/QuestionCard.js";
5
+ import { useChatWidget } from "./context.js";
6
+ import { isBlockingQuestion } from "./questions.js";
7
+ function TurnQuestion({ turn }) {
8
+ const { turns, live, answerQuestion, skipQuestion } = useChatWidget();
9
+ const set = turn.question;
10
+ const newest = turns[turns.length - 1]?.id === turn.id;
11
+ const blocking = newest && isBlockingQuestion(turn, live !== null);
12
+ const ref = useRef(null);
13
+ useEffect(() => {
14
+ if (!blocking) return;
15
+ ref.current?.querySelector("button:not(:disabled)")?.focus({ preventScroll: true });
16
+ }, []);
17
+ if (!set) return null;
18
+ return /* @__PURE__ */ jsx(
19
+ QuestionCard,
20
+ {
21
+ ref,
22
+ className: "ds-chat-widget__question",
23
+ questions: set.questions,
24
+ values: turn.answers,
25
+ disabled: turn.answers === void 0 && (!newest || live !== null),
26
+ onValuesChange: (answers) => answerQuestion(turn.id, answers),
27
+ onSkip: blocking ? () => skipQuestion(turn.id) : void 0
28
+ }
29
+ );
30
+ }
31
+ function ReplyBody({ reply, text }) {
32
+ if (!reply || reply.items.length === 1) return /* @__PURE__ */ jsx(Fragment, { children: text });
33
+ return /* @__PURE__ */ jsx(QuestionSummary, { variant: "plain", items: reply.items });
34
+ }
35
+ export {
36
+ ReplyBody,
37
+ TurnQuestion
38
+ };