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
@@ -20,7 +20,44 @@ const PLAN_STEPS = [
20
20
  { label: "Invite the three teammates", status: "active" },
21
21
  { label: "Post the welcome note", status: "pending" }
22
22
  ];
23
+ const KICKOFF_STEPS = [
24
+ { label: "Hold the slot on every calendar", status: "completed" },
25
+ { label: "Draft the agenda from the launch plan", status: "active" },
26
+ { label: "Send the invites", status: "pending" }
27
+ ];
23
28
  const card = (kind, data) => ({ kind, data });
29
+ function planFor(answer) {
30
+ const size = answer.toLowerCase();
31
+ const plan = size.includes("just me") ? "the Starter plan covers you: unlimited projects, no seats to manage" : size.includes("more than 50") ? "the Business plan fits: single sign-on, audit logs and pooled seats" : "the Team plan fits: shared workspaces for everyone, and guest seats stay free";
32
+ return {
33
+ steps: [{ status: "Matching the plan", point: "Team size decides the plan; nothing else differs." }],
34
+ text: `For ${answer.trim().replace(/[.!?]$/, "")}, ${plan}.`,
35
+ followups: ["What do the plans include?", "Invite my team to a workspace", "Set up notifications for me"]
36
+ };
37
+ }
38
+ const INVITES_SENT = {
39
+ steps: [
40
+ { status: "Sending the invites", point: "Three teammates to invite, all editors." },
41
+ { status: "Confirming delivery", point: "One call covers the batch." }
42
+ ],
43
+ text: "All three invites are out. Everyone lands in the workspace with editor access.",
44
+ card: card("tool-call", {
45
+ name: "invite_teammates",
46
+ summary: "3 editors to acme-design",
47
+ status: "success",
48
+ duration: "0.6s",
49
+ args: '{\n "workspace": "acme-design",\n "role": "editor",\n "emails": 3\n}'
50
+ }),
51
+ followups: ["Set up notifications for me", "Take us up to twelve seats", "Plan our launch kickoff"]
52
+ };
53
+ const PLANS_COMPARED = {
54
+ steps: [
55
+ { status: "Comparing the plans", point: "Three plans differ on collaboration, not on limits." },
56
+ { status: "Drafting the answer", point: "Name what the Team plan adds over the base." }
57
+ ],
58
+ text: "Every plan includes unlimited projects. The Team plan adds shared workspaces, guest seats, and a 90 day version history.",
59
+ followups: ["Which plan fits a team of five?", "Help me pick a plan", "Where is that written down?"]
60
+ };
24
61
  const DEMO_STARTERS = [
25
62
  { id: "start", label: "How do I get started?" },
26
63
  { id: "plans", label: "What do the plans include?" },
@@ -42,16 +79,7 @@ const DEMO_SCRIPT = scriptChat().user("How do I get started?").assistant(
42
79
  ],
43
80
  card: card("plan", { steps: PLAN_STEPS }),
44
81
  followups: ["Find a time we can all meet", "Take us up to twelve seats", "How is the trial going?"]
45
- }).user("What do the plans include?").assistant(
46
- "Every plan includes unlimited projects. The Team plan adds shared workspaces, guest seats, and a 90 day version history.",
47
- {
48
- steps: [
49
- { status: "Comparing the plans", point: "Three plans differ on collaboration, not on limits." },
50
- { status: "Drafting the answer", point: "Name what the Team plan adds over the base." }
51
- ],
52
- followups: ["Which plan fits a team of five?", "Where is that written down?", "How is the trial going?"]
53
- }
54
- ).user("Which plan fits a team of five?").assistant("The Team plan fits what you described: shared workspaces for the whole team, and guest seats stay free.", {
82
+ }).user("What do the plans include?").assistant(PLANS_COMPARED.text, PLANS_COMPARED).user("Which plan fits a team of five?").assistant("The Team plan fits what you described: shared workspaces for the whole team, and guest seats stay free. Shall we go with it?", {
55
83
  steps: [
56
84
  { status: "Matching plans to the team", point: "The visitor needs shared workspaces and guest seats." },
57
85
  { status: "Drafting the answer", point: "The Team plan covers both; lead with the card." }
@@ -59,13 +87,26 @@ const DEMO_SCRIPT = scriptChat().user("How do I get started?").assistant(
59
87
  card: card("product-card", {
60
88
  title: "Team plan",
61
89
  description: "Shared workspaces, guest seats, and version history for the whole team.",
62
- meta: "Billed monthly, cancel any time",
63
- actions: [
64
- { label: "Choose plan", variant: "secondary" },
65
- { label: "Compare plans", variant: "tertiary" }
66
- ]
90
+ meta: "Billed monthly, cancel any time"
67
91
  }),
68
- followups: ["Where is that written down?", "Invite my team to a workspace", "How is the trial going?"]
92
+ question: {
93
+ id: "team-plan",
94
+ questions: [
95
+ {
96
+ id: "plan",
97
+ prompt: "Go with the Team plan?",
98
+ options: [
99
+ { value: "choose", label: "Choose the Team plan" },
100
+ { value: "compare", label: "Compare the plans first" }
101
+ ]
102
+ }
103
+ ]
104
+ },
105
+ answered: (answer) => answer.toLowerCase().includes("compare") ? PLANS_COMPARED : {
106
+ steps: [{ status: "Switching the plan", point: "Move the workspace to Team from today." }],
107
+ text: "Done. The workspace is on the Team plan from today, billed monthly, and you can step back down any time.",
108
+ followups: ["Invite my team to a workspace", "Where is that written down?", "How is the trial going?"]
109
+ }
69
110
  }).user("Where is that written down?").assistant("Each chip opens the page the answer came from, so nothing here needs taking on trust.", {
70
111
  steps: [
71
112
  { status: "Collecting the sources", point: "Three product pages back the answer." },
@@ -112,26 +153,30 @@ const DEMO_SCRIPT = scriptChat().user("How do I get started?").assistant(
112
153
  label: "Signups"
113
154
  }),
114
155
  followups: ["Which plan fits a team of five?", "Invite my team to a workspace", "Where is that written down?"]
115
- }).user("Invite my team to a workspace").assistant("Happy to. Give me the word and I will send invites to the three teammates on your account, all as editors.", {
156
+ }).user("Invite my team to a workspace").assistant("Happy to. Shall I send invites to the three teammates on your account, all as editors?", {
116
157
  steps: [
117
158
  { status: "Checking the account", point: "Three teammates sit on the account, none invited yet." },
118
159
  { status: "Drafting the answer", point: "Confirm before sending anything." }
119
160
  ],
120
- followups: ["Go ahead and invite the other three", "Take us up to twelve seats", "Find a time we can all meet"]
121
- }).user("Go ahead and invite the other three").assistant("All three invites are out. Everyone lands in the workspace with editor access.", {
122
- steps: [
123
- { status: "Sending the invites", point: "Three teammates to invite, all editors." },
124
- { status: "Confirming delivery", point: "One call covers the batch." }
125
- ],
126
- card: card("tool-call", {
127
- name: "invite_teammates",
128
- summary: "3 editors to acme-design",
129
- status: "success",
130
- duration: "0.6s",
131
- args: '{\n "workspace": "acme-design",\n "role": "editor",\n "emails": 3\n}'
132
- }),
133
- followups: ["Find a time we can all meet", "Take us up to twelve seats", "How is the trial going?"]
134
- }).user("Take us up to twelve seats").assistant("Only the seat count moves, from five to twelve. Everything else in the config stays as it was.", {
161
+ question: {
162
+ id: "invites",
163
+ questions: [
164
+ {
165
+ id: "send",
166
+ prompt: "Send the invites?",
167
+ options: [
168
+ { value: "send", label: "Invite all three" },
169
+ { value: "wait", label: "Not yet" }
170
+ ]
171
+ }
172
+ ]
173
+ },
174
+ answered: (answer) => answer.toLowerCase().startsWith("not yet") ? {
175
+ steps: [{ status: "Holding the invites", point: "Nothing sent; keep the list ready." }],
176
+ text: "Nothing sent. The three are ready to go whenever you are.",
177
+ followups: ["Take us up to twelve seats", "Find a time we can all meet", "How is the trial going?"]
178
+ } : INVITES_SENT
179
+ }).user("Go ahead and invite the other three").assistant(INVITES_SENT.text, INVITES_SENT).user("Take us up to twelve seats").assistant("Only the seat count moves, from five to twelve. Everything else in the config stays as it was. Apply it?", {
135
180
  steps: [
136
181
  { status: "Locating the seat limit", point: "The seat limit lives in workspace.config.ts." },
137
182
  { status: "Preparing the change", point: "One line changes; show the diff." }
@@ -140,7 +185,34 @@ const DEMO_SCRIPT = scriptChat().user("How do I get started?").assistant(
140
185
  filename: "workspace.config.ts",
141
186
  diff: '@@ -12,7 +12,7 @@\n export const workspace = {\n name: "acme-design",\n- seats: 5,\n+ seats: 12,\n region: "eu-west",\n };'
142
187
  }),
143
- followups: ["Go ahead and invite the other three", "What do the plans include?", "Find a time we can all meet"]
188
+ question: {
189
+ id: "seats",
190
+ questions: [
191
+ {
192
+ id: "apply",
193
+ prompt: "Apply the change?",
194
+ options: [
195
+ { value: "apply", label: "Apply it" },
196
+ { value: "wait", label: "Not yet" }
197
+ ]
198
+ }
199
+ ]
200
+ },
201
+ answered: (answer) => answer.toLowerCase().startsWith("not yet") ? {
202
+ steps: [{ status: "Leaving the config", point: "The diff stays a proposal." }],
203
+ text: "Left as it is. The workspace stays at five seats until you say otherwise.",
204
+ followups: ["What do the plans include?", "Invite my team to a workspace", "How is the trial going?"]
205
+ } : {
206
+ steps: [{ status: "Applying the change", point: "One line in workspace.config.ts." }],
207
+ text: "Applied. The workspace takes twelve seats from now, and the next invoice picks up the difference.",
208
+ card: card("tool-call", {
209
+ name: "update_config",
210
+ summary: "workspace.config.ts, seats 5 to 12",
211
+ status: "success",
212
+ duration: "0.4s"
213
+ }),
214
+ followups: ["Invite my team to a workspace", "Find a time we can all meet", "How is the trial going?"]
215
+ }
144
216
  }).user("Here is the launch plan we are working from.").assistant(
145
217
  "Three phases, and most of the budget sits behind launch week itself. The dates line up with the workspace you already set up, so nothing needs to move.",
146
218
  {
@@ -169,31 +241,73 @@ const DEMO_SCRIPT = scriptChat().user("How do I get started?").assistant(
169
241
  }),
170
242
  followups: ["Set it all up for me", "Take us up to twelve seats", "Find a time we can all meet"]
171
243
  }
172
- ).user("Sync the shared calendar").assistant("The sync timed out on the provider's side. Retry below and it should land.", {
244
+ ).user("Sync the shared calendar").assistant("The sync timed out on the provider's side. It usually lands on a second try. Run it again?", {
173
245
  steps: [{ status: "Starting the calendar sync", point: "The calendar sync runs after the invites." }],
174
246
  card: card("tool-call", {
175
247
  name: "sync_calendar",
176
248
  summary: "the shared team calendar",
177
249
  status: "error",
178
- duration: "2.1s",
179
- retryable: true
250
+ duration: "2.1s"
180
251
  }),
181
- followups: ["Find a time we can all meet", "Go ahead and invite the other three", "How is the trial going?"]
182
- }).user("Find a time we can all meet").assistant("I can check everyone's calendar for a slot, once you allow it below.", {
252
+ question: {
253
+ id: "sync-retry",
254
+ questions: [
255
+ {
256
+ id: "retry",
257
+ prompt: "Try the sync again?",
258
+ options: [
259
+ { value: "retry", label: "Retry the sync" },
260
+ { value: "leave", label: "Leave it for now" }
261
+ ]
262
+ }
263
+ ]
264
+ },
265
+ answered: (answer) => answer.toLowerCase().startsWith("leave") ? {
266
+ steps: [{ status: "Leaving the sync", point: "Nothing to retry until asked." }],
267
+ text: "Left for now. Nothing else depends on it, and it can run again whenever you like.",
268
+ followups: ["Find a time we can all meet", "Invite my team to a workspace", "How is the trial going?"]
269
+ } : {
270
+ steps: [{ status: "Retrying the sync", point: "Second attempt against the provider." }],
271
+ text: "Second time lucky. The shared calendar is in sync.",
272
+ card: card("tool-call", {
273
+ name: "sync_calendar",
274
+ summary: "the shared team calendar",
275
+ status: "success",
276
+ duration: "1.2s"
277
+ }),
278
+ followups: ["Find a time we can all meet", "Plan our launch kickoff", "How is the trial going?"]
279
+ }
280
+ }).user("Find a time we can all meet").assistant("I can check everyone's calendar for a slot. Can I read the shared team calendar?", {
183
281
  steps: [
184
282
  { status: "Checking what this needs", point: "Suggesting times needs calendar access." },
185
283
  { status: "Preparing the request", point: "Ask before connecting anything." }
186
284
  ],
187
- card: card("interrupt", {
188
- title: "Connect the shared calendar?",
189
- description: "The assistant wants read access to the team calendar to suggest meeting times.",
190
- options: [
191
- { value: "allow", label: "Allow once", variant: "primary" },
192
- { value: "always", label: "Always allow", variant: "secondary" },
193
- { value: "deny", label: "Not now", variant: "secondary" }
285
+ question: {
286
+ id: "calendar-access",
287
+ questions: [
288
+ {
289
+ id: "access",
290
+ prompt: "Read the shared calendar?",
291
+ options: [
292
+ { value: "once", label: "Yes, just this once" },
293
+ { value: "always", label: "Yes, always" },
294
+ { value: "no", label: "Not now" }
295
+ ]
296
+ }
194
297
  ]
195
- }),
196
- followups: ["Go ahead and invite the other three", "How is the trial going?", "How do I get started?"]
298
+ },
299
+ answered: (answer) => answer.toLowerCase().startsWith("not now") ? {
300
+ steps: [{ status: "Leaving the calendar alone", point: "No access; offer the manual route." }],
301
+ text: "No problem, nothing is connected. You can share a few times that suit you and I will send them round.",
302
+ followups: ["Go ahead and invite the other three", "How is the trial going?", "How do I get started?"]
303
+ } : {
304
+ steps: [
305
+ { status: "Reading the shared calendar", point: "Four calendars, the next ten working days." },
306
+ { status: "Finding a free slot", point: "Thursday afternoon is clear for everyone." }
307
+ ],
308
+ text: answer.toLowerCase().includes("always") ? "Connected for good. Thursday at 2pm is the first slot everyone has free, and I will keep an eye out when plans change." : "Read once and disconnected. Thursday at 2pm is the first slot everyone has free.",
309
+ followups: ["Plan our launch kickoff", "Go ahead and invite the other three", "How is the trial going?"]
310
+ }
197
311
  }).user("Build me a launch dashboard").assistant(
198
312
  "Here is the launch on one board: what is running now, the trial signups, the projects and what is waiting on you. The rows that ask a question will ask it here.",
199
313
  {
@@ -214,7 +328,99 @@ const DEMO_SCRIPT = scriptChat().user("How do I get started?").assistant(
214
328
  card: artifactCard(DEMO_PAGE_ARTIFACT),
215
329
  followups: ["Build me a launch dashboard", "What do the plans include?", "How is the trial going?"]
216
330
  }
217
- ).fallback(
331
+ ).user("Help me pick a plan").assistant("Happy to. How big is the team that will use the workspace?", {
332
+ steps: [{ status: "Reading the plans", point: "Plans differ by team size; ask before recommending." }],
333
+ question: {
334
+ id: "team-size",
335
+ questions: [
336
+ {
337
+ id: "size",
338
+ prompt: "How big is the team?",
339
+ options: [
340
+ { value: "solo", label: "Just me" },
341
+ { value: "small", label: "2 to 10 people" },
342
+ { value: "medium", label: "11 to 50 people" },
343
+ { value: "large", label: "More than 50" }
344
+ ],
345
+ other: true
346
+ }
347
+ ]
348
+ },
349
+ answered: planFor
350
+ }).user("Set up notifications for me").assistant("Sure. Which of these should reach you? Pick as many as you like.", {
351
+ steps: [{ status: "Reading your settings", point: "Four kinds of notification; ask which matter." }],
352
+ question: {
353
+ id: "notifications",
354
+ questions: [
355
+ {
356
+ id: "kinds",
357
+ prompt: "Notify me about",
358
+ mode: "multi",
359
+ options: [
360
+ { value: "mentions", label: "Mentions", description: "When someone writes your name" },
361
+ { value: "comments", label: "Comments on my files" },
362
+ { value: "invites", label: "Invites accepted" },
363
+ { value: "summary", label: "A weekly summary", description: "Mondays, one email" }
364
+ ],
365
+ other: true
366
+ }
367
+ ]
368
+ },
369
+ answered: (answer) => ({
370
+ steps: [{ status: "Saving your settings", point: "Turn on what was picked, everything else off." }],
371
+ text: `Done. You will hear about ${answer.trim().toLowerCase()}, and nothing else. Change it any time in Settings.`,
372
+ followups: ["Plan our launch kickoff", "Help me pick a plan", "How is the trial going?"]
373
+ })
374
+ }).user("Plan our launch kickoff").assistant("Three quick questions first, so the kickoff fits the team.", {
375
+ steps: [
376
+ { status: "Reading the launch plan", point: "The kickoff needs a date, a guest list and a format." },
377
+ { status: "Preparing the questions", point: "Ask all three before booking anything." }
378
+ ],
379
+ question: {
380
+ id: "kickoff",
381
+ questions: [
382
+ {
383
+ id: "when",
384
+ prompt: "When should it happen?",
385
+ options: [
386
+ { value: "this-week", label: "This week" },
387
+ { value: "next-week", label: "Next week" },
388
+ { value: "after-beta", label: "After the beta" }
389
+ ]
390
+ },
391
+ {
392
+ id: "who",
393
+ prompt: "Who should be there?",
394
+ mode: "multi",
395
+ options: [
396
+ { value: "design", label: "Design" },
397
+ { value: "engineering", label: "Engineering" },
398
+ { value: "marketing", label: "Marketing" },
399
+ { value: "sales", label: "Sales" }
400
+ ]
401
+ },
402
+ {
403
+ id: "format",
404
+ prompt: "What format?",
405
+ options: [
406
+ { value: "room", label: "In person" },
407
+ { value: "video", label: "A video call" },
408
+ { value: "async", label: "An async update" }
409
+ ],
410
+ other: true
411
+ }
412
+ ]
413
+ },
414
+ answered: {
415
+ steps: [
416
+ { status: "Booking the kickoff", point: "Find the first slot everyone named is free." },
417
+ { status: "Drafting the agenda", point: "Lead with the launch dates, then owners." }
418
+ ],
419
+ text: "Kickoff is on the calendar. The slot is held for everyone you named, and the agenda is being drafted from the launch plan.",
420
+ card: card("plan", { steps: KICKOFF_STEPS }),
421
+ followups: ["Find a time we can all meet", "Build me a launch dashboard", "Set up notifications for me"]
422
+ }
423
+ }).fallback(
218
424
  "The launch is on track overall. Three of the four readiness tracks are green, and the one item I would watch is the **legal review**, since it gates the pricing rollout.",
219
425
  {
220
426
  steps: [
@@ -225,10 +431,10 @@ const DEMO_SCRIPT = scriptChat().user("How do I get started?").assistant(
225
431
  followups: ["Which track is behind?", "Who owns the legal review?", "What happens if pricing slips?"]
226
432
  }
227
433
  ).fallback(
228
- "This chat is a scripted preview of the widget, so free text gets one of a few written replies. The suggestion chips lead through the parts of the story that show off the rich content: a plan, a chart, a diff, a tool call, an approval and an artifact.",
434
+ "This chat is a scripted preview of the widget, so free text gets one of a few written replies. The suggestion chips lead through the parts of the story that show off the rich content: a plan, a chart, a diff, a tool call, an artifact and questions to answer.",
229
435
  {
230
436
  steps: [{ status: "Reading the question", point: "Nothing scripted matches this one." }],
231
- followups: ["How do I get started?", "What do the plans include?", "How is the trial going?"]
437
+ followups: ["How do I get started?", "Help me pick a plan", "Plan our launch kickoff"]
232
438
  }
233
439
  ).build();
234
440
  export {
@@ -31,8 +31,8 @@ export declare function createFollowupsFetcher(endpoint?: string): FollowupsSour
31
31
  /**
32
32
  * The turn to ask about, or null when there is nothing to ask about yet:
33
33
  * the newest turn must be a committed assistant answer that came from a
34
- * backend (an exchange id is the marker, as it is for the thumbs) and has
35
- * not been asked about already.
34
+ * backend (an exchange id is the marker, as it is for the thumbs), has
35
+ * not been asked about already, and asks no question of its own.
36
36
  */
37
37
  export declare function followupTarget(turns: ChatTurn[]): {
38
38
  id: string;
package/chat/followups.js CHANGED
@@ -41,6 +41,7 @@ function followupTarget(turns) {
41
41
  const last = turns[turns.length - 1];
42
42
  if (!last || last.role !== "assistant") return null;
43
43
  if (!last.exchangeId || last.followups || last.text === "") return null;
44
+ if (last.question) return null;
44
45
  const question = turns[turns.length - 2];
45
46
  if (!question || question.role !== "user") return null;
46
47
  return { id: last.id, question: question.text, answer: last.text };
package/chat/index.d.ts CHANGED
@@ -29,9 +29,10 @@ export { createFetchTransport, DEFAULT_FETCH_COPY, FIRST_BYTE_TIMEOUT_MS, MAX_TR
29
29
  export { scriptChat, createSimTransport, defaultFilesReply, DEFAULT_FALLBACK_REPLY, DEFAULT_SIM_TIMING, type ChatScript, type ChatScriptBuilder, type ScriptNode, type ScriptStep, type ScriptedReply, type SimTiming, } from './sim';
30
30
  export { createFollowupsFetcher, followupTarget, pickFallbackFollowups, DEFAULT_FALLBACK_FOLLOWUPS, MAX_FOLLOWUPS, FOLLOWUPS_TIMEOUT_MS, type FollowupsSource, } from './followups';
31
31
  export { SUGGESTION_MAX_CHARS, fitsChip } from './suggestions';
32
+ export { sanitizeQuestionSet, describeAnswers, serializeReply, isBlockingQuestion, ASK_USER_INPUT_SCHEMA, MAX_QUESTIONS, MAX_QUESTION_OPTIONS, MIN_QUESTION_OPTIONS, QUESTION_PROMPT_MAX_CHARS, OPTION_DESCRIPTION_MAX_CHARS, } from './questions';
32
33
  export { STARTER_COUNT, pickStarters, namedStarter, hashString, mulberry32, } from './starters';
33
34
  export { greetingFor, readGreeting, serverGreeting, subscribeClock, DEFAULT_GREETING_COPY, type GreetingCopy, } from './greeting';
34
35
  export { CHAT_PLACEMENTS, chatPlacement, DOCK_MIN_WIDTH, DOCK_MAX_WIDTH, FLOAT_MAX_SCALE, FLOAT_EDGE_GAP, dockWidthFromDrag, isFloatDragStart, clampFloatPosition, type ChatPlacement, type ChatSide, } from './placement';
35
36
  export { artifactCard, cardArtifact } from './artifacts';
36
- export { ARTIFACT_CARD_KIND, CHAT_EVENT_KINDS, type ChatArtifact, type ChatCard, type ChatEvent, type ChatTransport, type ChatTransportMessage, type ChatTurn, type ChatModelOption, type ChatFeedbackVerdict, type LivePhase, type LiveResponse, type Starter, } from './types';
37
+ export { ARTIFACT_CARD_KIND, CHAT_EVENT_KINDS, type ChatArtifact, type ChatCard, type ChatEvent, type ChatQuestion, type ChatQuestionAnswer, type ChatQuestionSet, type ChatReply, type ChatTransport, type ChatTransportMessage, type ChatTurn, type ChatModelOption, type ChatFeedbackVerdict, type LivePhase, type LiveResponse, type Starter, } from './types';
37
38
  export { chatWidget, chatWidgetRegistry, WIDGET_COUNT, type ChatWidgetMeta } from './registry';
package/chat/index.js CHANGED
@@ -14,6 +14,7 @@ import { DEFAULT_FETCH_COPY, FIRST_BYTE_TIMEOUT_MS, MAX_TRANSPORT_TURNS, createF
14
14
  import { DEFAULT_FALLBACK_REPLY, DEFAULT_SIM_TIMING, createSimTransport, defaultFilesReply, scriptChat } from "./sim.js";
15
15
  import { DEFAULT_FALLBACK_FOLLOWUPS, FOLLOWUPS_TIMEOUT_MS, MAX_FOLLOWUPS, createFollowupsFetcher, followupTarget, pickFallbackFollowups } from "./followups.js";
16
16
  import { SUGGESTION_MAX_CHARS, fitsChip } from "./suggestions.js";
17
+ import { ASK_USER_INPUT_SCHEMA, MAX_QUESTIONS, MAX_QUESTION_OPTIONS, MIN_QUESTION_OPTIONS, OPTION_DESCRIPTION_MAX_CHARS, QUESTION_PROMPT_MAX_CHARS, describeAnswers, isBlockingQuestion, sanitizeQuestionSet, serializeReply } from "./questions.js";
17
18
  import { STARTER_COUNT, hashString, mulberry32, namedStarter, pickStarters } from "./starters.js";
18
19
  import { DEFAULT_GREETING_COPY, greetingFor, readGreeting, serverGreeting, subscribeClock } from "./greeting.js";
19
20
  import { CHAT_PLACEMENTS, DOCK_MAX_WIDTH, DOCK_MIN_WIDTH, FLOAT_EDGE_GAP, FLOAT_MAX_SCALE, chatPlacement, clampFloatPosition, dockWidthFromDrag, isFloatDragStart } from "./placement.js";
@@ -25,6 +26,7 @@ export {
25
26
  ARTIFACT_MIN_COLUMN_WIDTH,
26
27
  ASIDE_RAIL_MIN_WIDTH,
27
28
  ASIDE_WITH_THREADS_MIN_WIDTH,
29
+ ASK_USER_INPUT_SCHEMA,
28
30
  CHAT_EVENT_KINDS,
29
31
  CHAT_PANEL_ID,
30
32
  CHAT_PLACEMENTS,
@@ -50,7 +52,12 @@ export {
50
52
  FLOAT_MAX_SCALE,
51
53
  FOLLOWUPS_TIMEOUT_MS,
52
54
  MAX_FOLLOWUPS,
55
+ MAX_QUESTIONS,
56
+ MAX_QUESTION_OPTIONS,
53
57
  MAX_TRANSPORT_TURNS,
58
+ MIN_QUESTION_OPTIONS,
59
+ OPTION_DESCRIPTION_MAX_CHARS,
60
+ QUESTION_PROMPT_MAX_CHARS,
54
61
  STARTER_COUNT,
55
62
  SUGGESTION_MAX_CHARS,
56
63
  TAKEOVER_QUERY,
@@ -68,11 +75,13 @@ export {
68
75
  createFollowupsFetcher,
69
76
  createSimTransport,
70
77
  defaultFilesReply,
78
+ describeAnswers,
71
79
  dockWidthFromDrag,
72
80
  fitsChip,
73
81
  followupTarget,
74
82
  greetingFor,
75
83
  hashString,
84
+ isBlockingQuestion,
76
85
  isFloatDragStart,
77
86
  isInternalPath,
78
87
  mulberry32,
@@ -80,7 +89,9 @@ export {
80
89
  pickFallbackFollowups,
81
90
  pickStarters,
82
91
  readGreeting,
92
+ sanitizeQuestionSet,
83
93
  scriptChat,
94
+ serializeReply,
84
95
  serverGreeting,
85
96
  subscribeClock,
86
97
  useChat,
package/chat/queries.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- export declare const DOCK_QUERY = "(min-width: 1440px)";
2
- export declare const TAKEOVER_QUERY = "(max-width: 719px)";
1
+ export declare const DOCK_QUERY: string;
2
+ export declare const TAKEOVER_QUERY: string;
3
3
  /** The widget's own width thresholds for seating its rails inline. */
4
4
  export declare const THREADS_RAIL_MIN_WIDTH = 720;
5
5
  /** The trailing seat's threshold, one number for both of its tenants: at
package/chat/queries.js CHANGED
@@ -1,7 +1,8 @@
1
1
  "use client";
2
2
  import { useSyncExternalStore } from "react";
3
- const DOCK_QUERY = "(min-width: 1440px)";
4
- const TAKEOVER_QUERY = "(max-width: 719px)";
3
+ import { mediaAbove, mediaBelow, BREAKPOINT_CHAT_TAKEOVER_PX, BREAKPOINT_CHAT_DOCK_PX } from "../tokens/breakpoints.js";
4
+ const DOCK_QUERY = mediaAbove(BREAKPOINT_CHAT_DOCK_PX);
5
+ const TAKEOVER_QUERY = mediaBelow(BREAKPOINT_CHAT_TAKEOVER_PX);
5
6
  const THREADS_RAIL_MIN_WIDTH = 720;
6
7
  const ASIDE_RAIL_MIN_WIDTH = 760;
7
8
  const ASIDE_WITH_THREADS_MIN_WIDTH = 1040;
@@ -0,0 +1,97 @@
1
+ import { ChatQuestionAnswer, ChatQuestionSet, ChatReply, ChatTurn } from './types';
2
+ /** Questions in one set. A longer interview belongs in a form, not a chat turn. */
3
+ export declare const MAX_QUESTIONS = 4;
4
+ /** Options in one question, before "Something else". */
5
+ export declare const MAX_QUESTION_OPTIONS = 5;
6
+ /** Options in one question at the least: one option is not a choice. */
7
+ export declare const MIN_QUESTION_OPTIONS = 2;
8
+ /** A question's own heading: a few words, one line in a docked panel. */
9
+ export declare const QUESTION_PROMPT_MAX_CHARS = 60;
10
+ /** An option's second line. */
11
+ export declare const OPTION_DESCRIPTION_MAX_CHARS = 80;
12
+ /**
13
+ * The question set the widget may render, or null. Questions that are
14
+ * malformed or over budget are dropped one by one; a set left with none,
15
+ * or with more than `MAX_QUESTIONS`, is dropped whole.
16
+ */
17
+ export declare function sanitizeQuestionSet(input: unknown, fallbackId?: string): ChatQuestionSet | null;
18
+ /**
19
+ * Whether a turn's questions hold the composer: a set of more than one,
20
+ * still open, on the newest turn. One question never does: typing instead
21
+ * of tapping is a fine answer to it, and leaves the card marked unanswered.
22
+ */
23
+ export declare function isBlockingQuestion(turn: ChatTurn | undefined, live: boolean): boolean;
24
+ /** What each answered question reads as: its prompt and the labels picked. */
25
+ export declare function describeAnswers(set: ChatQuestionSet, answers: ChatQuestionAnswer[]): ChatReply;
26
+ /**
27
+ * The answers as the plain message the model reads. One question reads as
28
+ * the answer alone ("Vite", "Components, Tokens"), because the model's own
29
+ * reply just asked it; a set names each question so the answers cannot be
30
+ * paired with the wrong one.
31
+ */
32
+ export declare function serializeReply(reply: ChatReply): string;
33
+ /**
34
+ * The `ask_user` tool's input schema, for `createChatHandler`. Exported so
35
+ * a host running its own loop asks for the same shape the widget renders.
36
+ */
37
+ export declare const ASK_USER_INPUT_SCHEMA: {
38
+ readonly type: "object";
39
+ readonly properties: {
40
+ readonly questions: {
41
+ readonly type: "array";
42
+ readonly minItems: 1;
43
+ readonly maxItems: 4;
44
+ readonly items: {
45
+ readonly type: "object";
46
+ readonly properties: {
47
+ readonly id: {
48
+ readonly type: "string";
49
+ readonly description: "A short stable key for the question, e.g. \"framework\".";
50
+ };
51
+ readonly prompt: {
52
+ readonly type: "string";
53
+ readonly description: "The question in a few words, at most 60 characters, e.g. \"Which framework?\".";
54
+ };
55
+ readonly mode: {
56
+ readonly type: "string";
57
+ readonly enum: readonly ["single", "multi"];
58
+ readonly description: "single when exactly one answer fits; multi when several can be true at once.";
59
+ };
60
+ readonly options: {
61
+ readonly type: "array";
62
+ readonly minItems: 2;
63
+ readonly maxItems: 5;
64
+ readonly items: {
65
+ readonly type: "object";
66
+ readonly properties: {
67
+ readonly label: {
68
+ readonly type: "string";
69
+ readonly description: "The answer as the person would say it, at most 40 characters.";
70
+ };
71
+ readonly description: {
72
+ readonly type: "string";
73
+ readonly description: "Optional: what choosing it means, at most 80 characters.";
74
+ };
75
+ };
76
+ readonly required: readonly ["label"];
77
+ };
78
+ };
79
+ readonly other: {
80
+ readonly type: "boolean";
81
+ readonly description: "Offer a free-text \"Something else\" when the options may not cover every answer.";
82
+ };
83
+ readonly min: {
84
+ readonly type: "integer";
85
+ readonly description: "multi only: the fewest picks that make an answer.";
86
+ };
87
+ readonly max: {
88
+ readonly type: "integer";
89
+ readonly description: "multi only: the most picks allowed.";
90
+ };
91
+ };
92
+ readonly required: readonly ["prompt", "options"];
93
+ };
94
+ };
95
+ };
96
+ readonly required: readonly ["questions"];
97
+ };