rift-ds 1.8.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 (111) 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/ChatAgentPane.d.ts +8 -5
  5. package/chat/ChatAgentPane.js +4 -1
  6. package/chat/ChatArtifactCard.d.ts +13 -0
  7. package/chat/ChatArtifactCard.js +21 -0
  8. package/chat/ChatArtifactPane.d.ts +18 -0
  9. package/chat/ChatArtifactPane.js +43 -0
  10. package/chat/ChatProvider.d.ts +4 -2
  11. package/chat/ChatProvider.js +15 -0
  12. package/chat/ChatWidget.css +213 -25
  13. package/chat/ChatWidget.d.ts +12 -1
  14. package/chat/ChatWidget.js +302 -75
  15. package/chat/DemoWelcome.js +17 -5
  16. package/chat/FollowupSuggestions.d.ts +7 -4
  17. package/chat/FollowupSuggestions.js +5 -3
  18. package/chat/TurnContent.d.ts +3 -1
  19. package/chat/TurnContent.js +9 -2
  20. package/chat/TurnQuestion.d.ts +19 -0
  21. package/chat/TurnQuestion.js +38 -0
  22. package/chat/artifacts.d.ts +5 -0
  23. package/chat/artifacts.js +14 -0
  24. package/chat/context.d.ts +18 -1
  25. package/chat/context.js +2 -0
  26. package/chat/demo-artifacts.d.ts +6 -0
  27. package/chat/demo-artifacts.js +80 -0
  28. package/chat/demo-card-components.d.ts +2 -0
  29. package/chat/demo-card-components.js +7 -0
  30. package/chat/demo-script.js +277 -49
  31. package/chat/demo.css +6 -0
  32. package/chat/demo.d.ts +1 -0
  33. package/chat/demo.js +4 -0
  34. package/chat/followups.d.ts +2 -2
  35. package/chat/followups.js +1 -0
  36. package/chat/index.d.ts +7 -3
  37. package/chat/index.js +23 -2
  38. package/chat/queries.d.ts +12 -3
  39. package/chat/queries.js +8 -3
  40. package/chat/questions.d.ts +97 -0
  41. package/chat/questions.js +140 -0
  42. package/chat/registry.json.d.ts +9 -3
  43. package/chat/registry.json.js +1 -1
  44. package/chat/server.d.ts +19 -0
  45. package/chat/server.js +45 -1
  46. package/chat/sim.d.ts +12 -1
  47. package/chat/sim.js +19 -3
  48. package/chat/types.d.ts +94 -1
  49. package/chat/types.js +3 -0
  50. package/chat/useChat.d.ts +16 -1
  51. package/chat/useChat.js +39 -2
  52. package/components/AgentRail/AgentRail.css +14 -5
  53. package/components/AgentRail/AgentRail.d.ts +2 -0
  54. package/components/AgentRail/AgentRail.js +3 -2
  55. package/components/AlertDialog/AlertDialog.css +1 -1
  56. package/components/AnchorNav/AnchorNav.css +1 -1
  57. package/components/AppSidebar/AppSidebar.css +5 -5
  58. package/components/AppSidebar/AppSidebar.js +3 -2
  59. package/components/ArtifactCard/ArtifactCard.css +106 -0
  60. package/components/ArtifactCard/ArtifactCard.d.ts +27 -0
  61. package/components/ArtifactCard/ArtifactCard.js +29 -0
  62. package/components/ArtifactPanel/ArtifactPanel.css +159 -0
  63. package/components/ArtifactPanel/ArtifactPanel.d.ts +70 -0
  64. package/components/ArtifactPanel/ArtifactPanel.js +131 -0
  65. package/components/Attachment/Attachment.css +3 -1
  66. package/components/BottomNav/BottomNav.css +1 -1
  67. package/components/Chart/Chart.css +1 -0
  68. package/components/ChatThread/ChatThread.css +21 -0
  69. package/components/ChatThread/ChatThread.d.ts +12 -1
  70. package/components/ChatThread/ChatThread.js +40 -1
  71. package/components/ColorPicker/ColorPicker.css +1 -1
  72. package/components/Combobox/Combobox.css +1 -1
  73. package/components/CommandPalette/CommandPalette.css +1 -1
  74. package/components/Composer/Composer.css +1 -1
  75. package/components/ContextMenu/ContextMenu.css +1 -1
  76. package/components/Dialog/Dialog.css +1 -1
  77. package/components/Drawer/Drawer.css +1 -1
  78. package/components/Dropdown/Dropdown.css +1 -1
  79. package/components/DropdownMenu/DropdownMenu.css +1 -1
  80. package/components/FilterBar/FilterBar.css +1 -1
  81. package/components/HoverCard/HoverCard.css +1 -1
  82. package/components/Lightbox/Lightbox.css +1 -1
  83. package/components/ModelPicker/ModelPicker.css +1 -1
  84. package/components/Popover/Popover.css +1 -1
  85. package/components/PromptSuggestions/PromptSuggestions.css +67 -0
  86. package/components/PromptSuggestions/PromptSuggestions.d.ts +8 -0
  87. package/components/PromptSuggestions/PromptSuggestions.js +34 -26
  88. package/components/QuestionCard/QuestionCard.css +358 -0
  89. package/components/QuestionCard/QuestionCard.d.ts +136 -0
  90. package/components/QuestionCard/QuestionCard.js +371 -0
  91. package/components/RichDropdown/RichDropdown.css +1 -1
  92. package/components/SourceChip/SourceChip.css +1 -1
  93. package/components/ThreadPanel/ThreadPanel.css +87 -33
  94. package/components/ThreadPanel/ThreadPanel.js +21 -10
  95. package/components/TimePicker/TimePicker.css +1 -1
  96. package/components/Toast/Toast.css +1 -1
  97. package/components/Tooltip/Tooltip.css +1 -1
  98. package/components/TopAppBar/TopAppBar.css +1 -1
  99. package/components/registry.json +24 -0
  100. package/components/registry.json.d.ts +24 -0
  101. package/components/registry.json.js +1 -1
  102. package/index.d.ts +3 -0
  103. package/index.js +7 -0
  104. package/package.json +5 -1
  105. package/tokens/breakpoints.d.ts +60 -0
  106. package/tokens/breakpoints.js +92 -0
  107. package/tokens/registry.d.ts +1 -1
  108. package/tokens/registry.json +14 -0
  109. package/tokens/registry.json.d.ts +14 -0
  110. package/tokens/registry.json.js +1 -1
  111. package/tokens/tokens-light.css +22 -0
package/chat/demo.css CHANGED
@@ -44,3 +44,9 @@
44
44
  .ds-chat-demo__logo--three {
45
45
  background-color: var(--color-chart-series-3);
46
46
  }
47
+
48
+ /* The dashboard artifact: the welcome board, given the inset a panel's
49
+ live content brings for itself. */
50
+ .ds-chat-demo__artifact-board {
51
+ padding: var(--padding-400);
52
+ }
package/chat/demo.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import { SimTiming } from './sim';
2
2
  import { ChatTransport } from './types';
3
3
  export { DEMO_RENDERERS, type DemoProductCard, type DemoChartCard, type DemoSourcesCard, type DemoToolCallCard, type DemoDiffCard, type DemoPlanCard, type DemoInterruptCard, type DemoStackCard, } from './demo-cards';
4
+ export { DEMO_ARTIFACT_RENDERERS, DEMO_DASHBOARD_ARTIFACT, DEMO_PAGE_ARTIFACT, } from './demo-artifacts';
4
5
  export { DEMO_SCRIPT, DEMO_STARTERS } from './demo-script';
5
6
  export { DEMO_MENTIONS, DEMO_MODELS, DEMO_THREAD_GROUPS, DEMO_PROJECTS, DEMO_THREAD_NAMES, DEMO_THREADS, DEMO_WELCOME_OPTIONS, type DemoWelcomeVariant, } from './demo-fixtures';
6
7
  export { demoAgentTabs } from './demo-agent';
package/chat/demo.js CHANGED
@@ -2,13 +2,17 @@ import { createSimTransport } from "./sim.js";
2
2
  import { DEMO_SCRIPT } from "./demo-script.js";
3
3
  import { DEMO_STARTERS } from "./demo-script.js";
4
4
  import { DEMO_RENDERERS } from "./demo-cards.js";
5
+ import { DEMO_ARTIFACT_RENDERERS, DEMO_DASHBOARD_ARTIFACT, DEMO_PAGE_ARTIFACT } from "./demo-artifacts.js";
5
6
  import { DEMO_MENTIONS, DEMO_MODELS, DEMO_PROJECTS, DEMO_THREADS, DEMO_THREAD_GROUPS, DEMO_THREAD_NAMES, DEMO_WELCOME_OPTIONS } from "./demo-fixtures.js";
6
7
  import { demoAgentTabs } from "./demo-agent.js";
7
8
  import { DemoBoard, DemoWelcome } from "./DemoWelcome.js";
8
9
  const createDemoTransport = (timing) => createSimTransport(DEMO_SCRIPT, timing);
9
10
  export {
11
+ DEMO_ARTIFACT_RENDERERS,
12
+ DEMO_DASHBOARD_ARTIFACT,
10
13
  DEMO_MENTIONS,
11
14
  DEMO_MODELS,
15
+ DEMO_PAGE_ARTIFACT,
12
16
  DEMO_PROJECTS,
13
17
  DEMO_RENDERERS,
14
18
  DEMO_SCRIPT,
@@ -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
@@ -19,16 +19,20 @@ export { ChatLauncher, CHAT_PANEL_ID, type ChatLauncherProps } from './ChatLaunc
19
19
  export { ChatThreads, type ChatThreadsProps } from './ChatThreads';
20
20
  export { ChatTabs, type ChatTabsProps } from './ChatTabs';
21
21
  export { ChatAgentPane, type ChatAgentPaneProps } from './ChatAgentPane';
22
- export { useChatWidget, isInternalPath, createFeedbackPoster, type ChatView, type ChatCardRenderers, type ChatLinkRenderer, type ChatWidgetContextValue, type FeedbackSink, } from './context';
23
- export { DOCK_QUERY, TAKEOVER_QUERY, THREADS_RAIL_MIN_WIDTH, ASIDE_RAIL_MIN_WIDTH, useTakeoverViewport, } from './queries';
22
+ export { ChatArtifactPane, type ChatArtifactPaneProps } from './ChatArtifactPane';
23
+ export { ChatArtifactCard, type ChatArtifactCardProps } from './ChatArtifactCard';
24
+ export { useChatWidget, isInternalPath, createFeedbackPoster, type ChatView, type ChatArtifactRenderers, type ChatCardRenderers, type ChatLinkRenderer, type ChatWidgetContextValue, type FeedbackSink, } from './context';
25
+ export { DOCK_QUERY, TAKEOVER_QUERY, THREADS_RAIL_MIN_WIDTH, ASIDE_RAIL_MIN_WIDTH, ARTIFACT_MIN_COLUMN_WIDTH, ASIDE_WITH_THREADS_MIN_WIDTH, useTakeoverViewport, } from './queries';
24
26
  export { useChat, DEFAULT_PLACEHOLDER_COPY, type ChatState, type ChatPlaceholderCopy, } from './useChat';
25
27
  export { useThreads, THREAD_NAMING_DELAY_MS, type ThreadsOptions, type ThreadsState } from './threads';
26
28
  export { createFetchTransport, DEFAULT_FETCH_COPY, FIRST_BYTE_TIMEOUT_MS, MAX_TRANSPORT_TURNS, type FetchTransportOptions, type FetchTransportCopy, } from './transport';
27
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';
28
30
  export { createFollowupsFetcher, followupTarget, pickFallbackFollowups, DEFAULT_FALLBACK_FOLLOWUPS, MAX_FOLLOWUPS, FOLLOWUPS_TIMEOUT_MS, type FollowupsSource, } from './followups';
29
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';
30
33
  export { STARTER_COUNT, pickStarters, namedStarter, hashString, mulberry32, } from './starters';
31
34
  export { greetingFor, readGreeting, serverGreeting, subscribeClock, DEFAULT_GREETING_COPY, type GreetingCopy, } from './greeting';
32
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';
33
- export { CHAT_EVENT_KINDS, type ChatCard, type ChatEvent, type ChatTransport, type ChatTransportMessage, type ChatTurn, type ChatModelOption, type ChatFeedbackVerdict, type LivePhase, type LiveResponse, type Starter, } from './types';
36
+ export { artifactCard, cardArtifact } from './artifacts';
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';
34
38
  export { chatWidget, chatWidgetRegistry, WIDGET_COUNT, type ChatWidgetMeta } from './registry';
package/chat/index.js CHANGED
@@ -4,25 +4,35 @@ import { CHAT_PANEL_ID, ChatLauncher } from "./ChatLauncher.js";
4
4
  import { ChatThreads } from "./ChatThreads.js";
5
5
  import { ChatTabs } from "./ChatTabs.js";
6
6
  import { ChatAgentPane } from "./ChatAgentPane.js";
7
+ import { ChatArtifactPane } from "./ChatArtifactPane.js";
8
+ import { ChatArtifactCard } from "./ChatArtifactCard.js";
7
9
  import { createFeedbackPoster, isInternalPath, useChatWidget } from "./context.js";
8
- import { ASIDE_RAIL_MIN_WIDTH, DOCK_QUERY, TAKEOVER_QUERY, THREADS_RAIL_MIN_WIDTH, useTakeoverViewport } from "./queries.js";
10
+ import { ARTIFACT_MIN_COLUMN_WIDTH, ASIDE_RAIL_MIN_WIDTH, ASIDE_WITH_THREADS_MIN_WIDTH, DOCK_QUERY, TAKEOVER_QUERY, THREADS_RAIL_MIN_WIDTH, useTakeoverViewport } from "./queries.js";
9
11
  import { DEFAULT_PLACEHOLDER_COPY, useChat } from "./useChat.js";
10
12
  import { THREAD_NAMING_DELAY_MS, useThreads } from "./threads.js";
11
13
  import { DEFAULT_FETCH_COPY, FIRST_BYTE_TIMEOUT_MS, MAX_TRANSPORT_TURNS, createFetchTransport } from "./transport.js";
12
14
  import { DEFAULT_FALLBACK_REPLY, DEFAULT_SIM_TIMING, createSimTransport, defaultFilesReply, scriptChat } from "./sim.js";
13
15
  import { DEFAULT_FALLBACK_FOLLOWUPS, FOLLOWUPS_TIMEOUT_MS, MAX_FOLLOWUPS, createFollowupsFetcher, followupTarget, pickFallbackFollowups } from "./followups.js";
14
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";
15
18
  import { STARTER_COUNT, hashString, mulberry32, namedStarter, pickStarters } from "./starters.js";
16
19
  import { DEFAULT_GREETING_COPY, greetingFor, readGreeting, serverGreeting, subscribeClock } from "./greeting.js";
17
20
  import { CHAT_PLACEMENTS, DOCK_MAX_WIDTH, DOCK_MIN_WIDTH, FLOAT_EDGE_GAP, FLOAT_MAX_SCALE, chatPlacement, clampFloatPosition, dockWidthFromDrag, isFloatDragStart } from "./placement.js";
18
- import { CHAT_EVENT_KINDS } from "./types.js";
21
+ import { artifactCard, cardArtifact } from "./artifacts.js";
22
+ import { ARTIFACT_CARD_KIND, CHAT_EVENT_KINDS } from "./types.js";
19
23
  import { WIDGET_COUNT, chatWidget, chatWidgetRegistry } from "./registry.js";
20
24
  export {
25
+ ARTIFACT_CARD_KIND,
26
+ ARTIFACT_MIN_COLUMN_WIDTH,
21
27
  ASIDE_RAIL_MIN_WIDTH,
28
+ ASIDE_WITH_THREADS_MIN_WIDTH,
29
+ ASK_USER_INPUT_SCHEMA,
22
30
  CHAT_EVENT_KINDS,
23
31
  CHAT_PANEL_ID,
24
32
  CHAT_PLACEMENTS,
25
33
  ChatAgentPane,
34
+ ChatArtifactCard,
35
+ ChatArtifactPane,
26
36
  ChatLauncher,
27
37
  ChatProvider,
28
38
  ChatTabs,
@@ -42,13 +52,20 @@ export {
42
52
  FLOAT_MAX_SCALE,
43
53
  FOLLOWUPS_TIMEOUT_MS,
44
54
  MAX_FOLLOWUPS,
55
+ MAX_QUESTIONS,
56
+ MAX_QUESTION_OPTIONS,
45
57
  MAX_TRANSPORT_TURNS,
58
+ MIN_QUESTION_OPTIONS,
59
+ OPTION_DESCRIPTION_MAX_CHARS,
60
+ QUESTION_PROMPT_MAX_CHARS,
46
61
  STARTER_COUNT,
47
62
  SUGGESTION_MAX_CHARS,
48
63
  TAKEOVER_QUERY,
49
64
  THREADS_RAIL_MIN_WIDTH,
50
65
  THREAD_NAMING_DELAY_MS,
51
66
  WIDGET_COUNT,
67
+ artifactCard,
68
+ cardArtifact,
52
69
  chatPlacement,
53
70
  chatWidget,
54
71
  chatWidgetRegistry,
@@ -58,11 +75,13 @@ export {
58
75
  createFollowupsFetcher,
59
76
  createSimTransport,
60
77
  defaultFilesReply,
78
+ describeAnswers,
61
79
  dockWidthFromDrag,
62
80
  fitsChip,
63
81
  followupTarget,
64
82
  greetingFor,
65
83
  hashString,
84
+ isBlockingQuestion,
66
85
  isFloatDragStart,
67
86
  isInternalPath,
68
87
  mulberry32,
@@ -70,7 +89,9 @@ export {
70
89
  pickFallbackFollowups,
71
90
  pickStarters,
72
91
  readGreeting,
92
+ sanitizeQuestionSet,
73
93
  scriptChat,
94
+ serializeReply,
74
95
  serverGreeting,
75
96
  subscribeClock,
76
97
  useChat,
package/chat/queries.d.ts CHANGED
@@ -1,8 +1,17 @@
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
- export declare const ASIDE_RAIL_MIN_WIDTH = 860;
5
+ /** The trailing seat's threshold, one number for both of its tenants: at
6
+ this width and up the agent pane and the artifact panel sit beside the
7
+ conversation, below it each rises as a bottom sheet. */
8
+ export declare const ASIDE_RAIL_MIN_WIDTH = 760;
9
+ /** Below this the inline threads rail yields its room to a trailing panel
10
+ seated inline, and goes back behind the header's hamburger. */
11
+ export declare const ASIDE_WITH_THREADS_MIN_WIDTH = 1040;
12
+ /** The floor either column keeps while an artifact is dragged wider or
13
+ narrower: the conversation never loses more than this, nor the panel. */
14
+ export declare const ARTIFACT_MIN_COLUMN_WIDTH = 320;
6
15
  /** Whether the viewport is a phone width (TAKEOVER_QUERY), false on the
7
16
  server and for the first client render. Shared by the launcher and any
8
17
  host stage, which both hand the widget its phone layout. */
package/chat/queries.js CHANGED
@@ -1,9 +1,12 @@
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
- const ASIDE_RAIL_MIN_WIDTH = 860;
7
+ const ASIDE_RAIL_MIN_WIDTH = 760;
8
+ const ASIDE_WITH_THREADS_MIN_WIDTH = 1040;
9
+ const ARTIFACT_MIN_COLUMN_WIDTH = 320;
7
10
  const subscribeTakeover = (onChange) => {
8
11
  const media = window.matchMedia(TAKEOVER_QUERY);
9
12
  media.addEventListener("change", onChange);
@@ -14,7 +17,9 @@ function useTakeoverViewport() {
14
17
  return useSyncExternalStore(subscribeTakeover, readTakeover, () => false);
15
18
  }
16
19
  export {
20
+ ARTIFACT_MIN_COLUMN_WIDTH,
17
21
  ASIDE_RAIL_MIN_WIDTH,
22
+ ASIDE_WITH_THREADS_MIN_WIDTH,
18
23
  DOCK_QUERY,
19
24
  TAKEOVER_QUERY,
20
25
  THREADS_RAIL_MIN_WIDTH,
@@ -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
+ };
@@ -0,0 +1,140 @@
1
+ import { SUGGESTION_MAX_CHARS, fitsChip } from "./suggestions.js";
2
+ const MAX_QUESTIONS = 4;
3
+ const MAX_QUESTION_OPTIONS = 5;
4
+ const MIN_QUESTION_OPTIONS = 2;
5
+ const QUESTION_PROMPT_MAX_CHARS = 60;
6
+ const OPTION_DESCRIPTION_MAX_CHARS = 80;
7
+ const str = (value) => typeof value === "string" ? value.trim() : "";
8
+ const slug = (text, index) => text.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 40) || `option-${index + 1}`;
9
+ function sanitizeQuestion(input, index) {
10
+ if (typeof input !== "object" || input === null) return null;
11
+ const raw = input;
12
+ const prompt = str(raw.prompt);
13
+ if (!prompt || prompt.length > QUESTION_PROMPT_MAX_CHARS) return null;
14
+ const mode = raw.mode === "multi" ? "multi" : "single";
15
+ if (!Array.isArray(raw.options)) return null;
16
+ const options = [];
17
+ const seen = /* @__PURE__ */ new Set();
18
+ for (const [optionIndex, entry] of raw.options.entries()) {
19
+ if (typeof entry !== "object" || entry === null) return null;
20
+ const option = entry;
21
+ const label = str(option.label);
22
+ if (!fitsChip(label)) return null;
23
+ const value = str(option.value) || slug(label, optionIndex);
24
+ if (seen.has(value) || seen.has(label.toLowerCase())) return null;
25
+ seen.add(value);
26
+ seen.add(label.toLowerCase());
27
+ const description = str(option.description);
28
+ options.push(
29
+ description && description.length <= OPTION_DESCRIPTION_MAX_CHARS ? { value, label, description } : { value, label }
30
+ );
31
+ }
32
+ if (options.length < MIN_QUESTION_OPTIONS || options.length > MAX_QUESTION_OPTIONS) return null;
33
+ const question = {
34
+ id: str(raw.id) || `q${index + 1}`,
35
+ prompt,
36
+ mode,
37
+ options
38
+ };
39
+ if (raw.other === true) question.other = true;
40
+ if (mode === "multi") {
41
+ const available = options.length + (question.other ? 1 : 0);
42
+ const min = typeof raw.min === "number" ? Math.floor(raw.min) : 1;
43
+ const max = typeof raw.max === "number" ? Math.floor(raw.max) : available;
44
+ if (min < 1 || max < min || max > available) return null;
45
+ if (min !== 1) question.min = min;
46
+ if (max !== available) question.max = max;
47
+ }
48
+ return question;
49
+ }
50
+ function sanitizeQuestionSet(input, fallbackId = "questions") {
51
+ if (typeof input !== "object" || input === null) return null;
52
+ const raw = input;
53
+ if (!Array.isArray(raw.questions) || raw.questions.length > MAX_QUESTIONS) return null;
54
+ const questions = raw.questions.map((question, index) => sanitizeQuestion(question, index)).filter((question) => question !== null);
55
+ if (questions.length === 0) return null;
56
+ if (new Set(questions.map((question) => question.id)).size !== questions.length) return null;
57
+ return { id: str(raw.id) || fallbackId, questions };
58
+ }
59
+ function isBlockingQuestion(turn, live) {
60
+ return !live && turn?.role === "assistant" && !!turn.question && turn.question.questions.length > 1 && turn.answers === void 0;
61
+ }
62
+ function describeAnswers(set, answers) {
63
+ return {
64
+ items: set.questions.map((question) => {
65
+ const answer = answers.find((entry) => entry.questionId === question.id);
66
+ const labels = question.options.filter((option) => answer?.values.includes(option.value)).map((option) => option.label);
67
+ if (answer?.other) labels.push(answer.other);
68
+ return { prompt: question.prompt, answers: labels };
69
+ })
70
+ };
71
+ }
72
+ function serializeReply(reply) {
73
+ const answered = (labels) => labels.length > 0 ? labels.join(", ") : "(no answer)";
74
+ if (reply.items.length === 1) return answered(reply.items[0].answers);
75
+ return reply.items.map((item) => `${item.prompt} ${answered(item.answers)}`).join("\n");
76
+ }
77
+ const ASK_USER_INPUT_SCHEMA = {
78
+ type: "object",
79
+ properties: {
80
+ questions: {
81
+ type: "array",
82
+ minItems: 1,
83
+ maxItems: MAX_QUESTIONS,
84
+ items: {
85
+ type: "object",
86
+ properties: {
87
+ id: { type: "string", description: 'A short stable key for the question, e.g. "framework".' },
88
+ prompt: {
89
+ type: "string",
90
+ description: `The question in a few words, at most ${QUESTION_PROMPT_MAX_CHARS} characters, e.g. "Which framework?".`
91
+ },
92
+ mode: {
93
+ type: "string",
94
+ enum: ["single", "multi"],
95
+ description: "single when exactly one answer fits; multi when several can be true at once."
96
+ },
97
+ options: {
98
+ type: "array",
99
+ minItems: MIN_QUESTION_OPTIONS,
100
+ maxItems: MAX_QUESTION_OPTIONS,
101
+ items: {
102
+ type: "object",
103
+ properties: {
104
+ label: {
105
+ type: "string",
106
+ description: `The answer as the person would say it, at most ${SUGGESTION_MAX_CHARS} characters.`
107
+ },
108
+ description: {
109
+ type: "string",
110
+ description: `Optional: what choosing it means, at most ${OPTION_DESCRIPTION_MAX_CHARS} characters.`
111
+ }
112
+ },
113
+ required: ["label"]
114
+ }
115
+ },
116
+ other: {
117
+ type: "boolean",
118
+ description: 'Offer a free-text "Something else" when the options may not cover every answer.'
119
+ },
120
+ min: { type: "integer", description: "multi only: the fewest picks that make an answer." },
121
+ max: { type: "integer", description: "multi only: the most picks allowed." }
122
+ },
123
+ required: ["prompt", "options"]
124
+ }
125
+ }
126
+ },
127
+ required: ["questions"]
128
+ };
129
+ export {
130
+ ASK_USER_INPUT_SCHEMA,
131
+ MAX_QUESTIONS,
132
+ MAX_QUESTION_OPTIONS,
133
+ MIN_QUESTION_OPTIONS,
134
+ OPTION_DESCRIPTION_MAX_CHARS,
135
+ QUESTION_PROMPT_MAX_CHARS,
136
+ describeAnswers,
137
+ isBlockingQuestion,
138
+ sanitizeQuestionSet,
139
+ serializeReply
140
+ };
@@ -6,11 +6,11 @@ declare const _default: {
6
6
  "slug": "ai-chat",
7
7
  "label": "AI chat",
8
8
  "category": "ai",
9
- "description": "The whole AI chat, assembled: the panel, the streaming replies, the history, the cards a reply can carry and the server handlers, set up with props.",
9
+ "description": "The whole AI chat: the panel, the streaming replies, the history, the cards and questions a reply can carry and the server handlers, set up with props.",
10
10
  "command": "init chat",
11
11
  "subpaths": {
12
12
  "./chat": "The widget, the provider, the launcher and the engine under them.",
13
- "./chat/server": "The streaming chat handler and its follow-ups and feedback siblings, for any web-standard route.",
13
+ "./chat/server": "The streaming chat handler, with an opt-in tool for asking the visitor questions, and its follow-ups and feedback siblings, for any web-standard route.",
14
14
  "./chat/upstash": "Rate limits, a daily budget and the exchange log over Upstash Redis, as the handlers' guardrails.",
15
15
  "./chat/demo": "A scripted conversation with rich cards and fixtures, for a preview that needs no key.",
16
16
  "./chat/chat.css": "The widget's stylesheet."
@@ -35,6 +35,8 @@ declare const _default: {
35
35
  "AgentRail",
36
36
  "AgentStatus",
37
37
  "AiButton",
38
+ "ArtifactCard",
39
+ "ArtifactPanel",
38
40
  "AttachmentDropZone",
39
41
  "AttachmentGroup",
40
42
  "ChatHeader",
@@ -44,11 +46,13 @@ declare const _default: {
44
46
  "CodeBlock",
45
47
  "Composer",
46
48
  "DropdownMenu",
49
+ "EmptyState",
47
50
  "Mention",
48
51
  "ModelPicker",
49
52
  "PixelAvatar",
50
53
  "PromptSuggestions",
51
54
  "Prose",
55
+ "QuestionCard",
52
56
  "Reasoning",
53
57
  "ThreadPanel",
54
58
  "ThreadTabs"
@@ -59,7 +63,9 @@ declare const _default: {
59
63
  "ChatLauncher",
60
64
  "ChatThreads",
61
65
  "ChatTabs",
62
- "ChatAgentPane"
66
+ "ChatAgentPane",
67
+ "ChatArtifactPane",
68
+ "ChatArtifactCard"
63
69
  ]
64
70
  }
65
71
  ]
@@ -1,5 +1,5 @@
1
1
  const $comment = "The widget registry: the fourth kind of thing the package ships, between the components and the templates. One member by design. `parts` names the library components the widget composes (held to src/components/registry.json by scripts/validate-chat-widget.mjs), `subpaths` the package exports it occupies (held to scripts/package-manifest.mjs), `category` the component category whose section and sidebar group its page joins (held to src/components/registry.json), `peers` the optional peer dependencies each subpath needs (held to package.json). The widget's page under /components, the /docs/chat page, the chat corpus and the MCP tools read this file; never type its facts elsewhere.";
2
- const widgets = [{ "name": "ChatWidget", "slug": "ai-chat", "label": "AI chat", "category": "ai", "description": "The whole AI chat, assembled: the panel, the streaming replies, the history, the cards a reply can carry and the server handlers, set up with props.", "command": "init chat", "subpaths": { "./chat": "The widget, the provider, the launcher and the engine under them.", "./chat/server": "The streaming chat handler and its follow-ups and feedback siblings, for any web-standard route.", "./chat/upstash": "Rate limits, a daily budget and the exchange log over Upstash Redis, as the handlers' guardrails.", "./chat/demo": "A scripted conversation with rich cards and fixtures, for a preview that needs no key.", "./chat/chat.css": "The widget's stylesheet." }, "peers": { "./chat": ["react-markdown", "remark-gfm"], "./chat/server": ["@anthropic-ai/sdk"], "./chat/upstash": ["@upstash/ratelimit", "@upstash/redis"], "./chat/demo": ["recharts"] }, "parts": ["AgentRail", "AgentStatus", "AiButton", "AttachmentDropZone", "AttachmentGroup", "ChatHeader", "ChatMessage", "ChatThread", "CircularButton", "CodeBlock", "Composer", "DropdownMenu", "Mention", "ModelPicker", "PixelAvatar", "PromptSuggestions", "Prose", "Reasoning", "ThreadPanel", "ThreadTabs"], "documented": ["ChatWidget", "ChatProvider", "ChatLauncher", "ChatThreads", "ChatTabs", "ChatAgentPane"] }];
2
+ const widgets = [{ "name": "ChatWidget", "slug": "ai-chat", "label": "AI chat", "category": "ai", "description": "The whole AI chat: the panel, the streaming replies, the history, the cards and questions a reply can carry and the server handlers, set up with props.", "command": "init chat", "subpaths": { "./chat": "The widget, the provider, the launcher and the engine under them.", "./chat/server": "The streaming chat handler, with an opt-in tool for asking the visitor questions, and its follow-ups and feedback siblings, for any web-standard route.", "./chat/upstash": "Rate limits, a daily budget and the exchange log over Upstash Redis, as the handlers' guardrails.", "./chat/demo": "A scripted conversation with rich cards and fixtures, for a preview that needs no key.", "./chat/chat.css": "The widget's stylesheet." }, "peers": { "./chat": ["react-markdown", "remark-gfm"], "./chat/server": ["@anthropic-ai/sdk"], "./chat/upstash": ["@upstash/ratelimit", "@upstash/redis"], "./chat/demo": ["recharts"] }, "parts": ["AgentRail", "AgentStatus", "AiButton", "ArtifactCard", "ArtifactPanel", "AttachmentDropZone", "AttachmentGroup", "ChatHeader", "ChatMessage", "ChatThread", "CircularButton", "CodeBlock", "Composer", "DropdownMenu", "EmptyState", "Mention", "ModelPicker", "PixelAvatar", "PromptSuggestions", "Prose", "QuestionCard", "Reasoning", "ThreadPanel", "ThreadTabs"], "documented": ["ChatWidget", "ChatProvider", "ChatLauncher", "ChatThreads", "ChatTabs", "ChatAgentPane", "ChatArtifactPane", "ChatArtifactCard"] }];
3
3
  const registry = {
4
4
  $comment,
5
5
  widgets
package/chat/server.d.ts CHANGED
@@ -16,6 +16,13 @@ export interface ChatTool {
16
16
  /** The trace point shown while the round runs, e.g. "Checked the Button prop contract". */
17
17
  tracePoint?: (input: Record<string, unknown>) => string;
18
18
  }
19
+ /** The name of the built-in tool the model asks the visitor questions through. */
20
+ export declare const ASK_USER_TOOL_NAME = "ask_user";
21
+ /**
22
+ * What the model is told about asking. Lives in the tool's description, not
23
+ * the system prompt, so switching it on never disturbs a cached prefix.
24
+ */
25
+ export declare const DEFAULT_ASK_USER_DESCRIPTION: string;
19
26
  /**
20
27
  * How much of the day's budget remains, as a model policy: `open` means the
21
28
  * default model serves, `reduced` means the budget model is the default (the
@@ -105,6 +112,8 @@ export interface ChatHandlerCopy {
105
112
  thinking: string;
106
113
  /** The status label while a tool round runs. */
107
114
  lookingUp: string;
115
+ /** The reply text when the model asks several questions without writing any. One question stands in for itself. */
116
+ questionsIntro: string;
108
117
  /** The error for a busy or overloaded model. */
109
118
  busy: string;
110
119
  /** The error for a rejected key. */
@@ -140,6 +149,16 @@ export interface ChatHandlerOptions {
140
149
  /** The wire value that serves once the budget steps down, and the only one past the lock. Defaults to `defaultModel`. */
141
150
  budgetModel?: string;
142
151
  tools?: ChatTool[];
152
+ /**
153
+ * Give the model the built-in `ask_user` tool, through which a reply can
154
+ * end in questions the widget draws as answers to pick. Pass an object to
155
+ * replace the guidance the model reads. Off by default. Calling it ends
156
+ * the exchange: no further model call is made, so it never counts against
157
+ * `maxModelCalls` beyond the pass that asked.
158
+ */
159
+ askUser?: boolean | {
160
+ description?: string;
161
+ };
143
162
  /** Model calls per exchange: the first pass plus tool rounds. The last allowed pass withholds the tools. */
144
163
  maxModelCalls?: number;
145
164
  /** Caps thinking and response text together. */
package/chat/server.js CHANGED
@@ -1,5 +1,15 @@
1
1
  import Anthropic from "@anthropic-ai/sdk";
2
+ import { ASK_USER_INPUT_SCHEMA, sanitizeQuestionSet } from "./questions.js";
2
3
  import { SUGGESTION_MAX_CHARS, fitsChip } from "./suggestions.js";
4
+ const ASK_USER_TOOL_NAME = "ask_user";
5
+ const DEFAULT_ASK_USER_DESCRIPTION = [
6
+ "Offer the person answers to pick for a question your reply has just asked in words.",
7
+ "Use it only when the most useful next step depends on their answer and the likely answers can be listed: which framework, which of these options, which parts apply.",
8
+ "Do not use it for open questions, for confirming something they already said, or when you can simply answer. Most replies should not ask.",
9
+ "Write the question in your reply first, then call this once, as your last action. The conversation waits for their answer.",
10
+ "Prefer one question. Use several (at most 4) only when you need all the answers before you can continue. mode is single when one answer fits and multi when several can be true together.",
11
+ "Set other to true when the options may not cover every answer."
12
+ ].join(" ");
3
13
  const DEFAULT_CHAT_LIMITS = {
4
14
  maxTurns: 10,
5
15
  maxMessageChars: 4e3,
@@ -15,6 +25,7 @@ const DEFAULT_CHAT_COPY = {
15
25
  reading: "Reading",
16
26
  thinking: "Thinking",
17
27
  lookingUp: "Looking it up",
28
+ questionsIntro: "A few questions before I go on.",
18
29
  busy: "The model is busy right now. Give it a moment and ask again.",
19
30
  unauthorised: "This chat is not configured correctly at the moment.",
20
31
  failed: "Something went wrong reaching the model. Try that again in a moment."
@@ -121,7 +132,15 @@ function createChatHandler(options) {
121
132
  const copy = { ...DEFAULT_CHAT_COPY, ...options.copy };
122
133
  const sanitise = options.sanitiseContext ?? sanitisePath;
123
134
  const tools = options.tools ?? [];
124
- const toolDefinitions = tools.map((tool) => tool.definition);
135
+ const askUserDefinition = options.askUser ? {
136
+ name: ASK_USER_TOOL_NAME,
137
+ description: (typeof options.askUser === "object" ? options.askUser.description : void 0) ?? DEFAULT_ASK_USER_DESCRIPTION,
138
+ input_schema: ASK_USER_INPUT_SCHEMA
139
+ } : null;
140
+ const toolDefinitions = [
141
+ ...tools.map((tool) => tool.definition),
142
+ ...askUserDefinition ? [askUserDefinition] : []
143
+ ];
125
144
  const toolByName = new Map(tools.map((tool) => [tool.definition.name, tool]));
126
145
  const maxModelCalls = options.maxModelCalls ?? 3;
127
146
  const maxTokens = options.maxTokens ?? 3e3;
@@ -273,7 +292,30 @@ function createChatHandler(options) {
273
292
  (block) => block.type === "tool_use"
274
293
  );
275
294
  const results = [];
295
+ const ask = askUserDefinition ? toolUses.find((block) => block.name === ASK_USER_TOOL_NAME) : void 0;
296
+ const set = ask ? sanitizeQuestionSet(ask.input, `${exchangeId}-questions`) : null;
297
+ if (set) {
298
+ if (!streamedText) {
299
+ const lead = set.questions.length === 1 ? set.questions[0].prompt : copy.questionsIntro;
300
+ reasoning.close();
301
+ streamedText = true;
302
+ firstTextMs = Date.now() - startedAt;
303
+ answerText += lead;
304
+ send({ type: "delta", text: lead });
305
+ }
306
+ send({ type: "question", set });
307
+ break;
308
+ }
276
309
  for (const block of toolUses) {
310
+ if (block.name === ASK_USER_TOOL_NAME) {
311
+ results.push({
312
+ type: "tool_result",
313
+ tool_use_id: block.id,
314
+ is_error: true,
315
+ content: `Those questions could not be shown. Each needs a short prompt and 2 to 5 distinct options of at most ${SUGGESTION_MAX_CHARS} characters, and a set holds at most 4. Fix them, or ask in plain text.`
316
+ });
317
+ continue;
318
+ }
277
319
  const args = typeof block.input === "object" && block.input !== null ? block.input : {};
278
320
  send({
279
321
  type: "status",
@@ -477,6 +519,8 @@ function createFeedbackHandler(options = {}) {
477
519
  };
478
520
  }
479
521
  export {
522
+ ASK_USER_TOOL_NAME,
523
+ DEFAULT_ASK_USER_DESCRIPTION,
480
524
  DEFAULT_CHAT_COPY,
481
525
  DEFAULT_CHAT_LIMITS,
482
526
  createChatHandler,