rift-ds 1.6.0 → 1.8.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 (133) hide show
  1. package/README.md +34 -13
  2. package/bin/rift-ds.mjs +231 -44
  3. package/bin/templates/chat/.env.chat.example +21 -0
  4. package/bin/templates/chat/app/api/chat/feedback/route.ts +12 -0
  5. package/bin/templates/chat/app/api/chat/followups/route.ts +23 -0
  6. package/bin/templates/chat/app/api/chat/guardrails.ts +18 -0
  7. package/bin/templates/chat/app/api/chat/route.ts +31 -0
  8. package/bin/templates/chat/components/chat/ChatMount.tsx +52 -0
  9. package/bin/templates/chat/components/chat/models.ts +29 -0
  10. package/chat/AssistantTurn.d.ts +5 -0
  11. package/chat/AssistantTurn.js +82 -0
  12. package/chat/ChatAgentPane.css +56 -0
  13. package/chat/ChatAgentPane.d.ts +21 -0
  14. package/chat/ChatAgentPane.js +25 -0
  15. package/chat/ChatLauncher.d.ts +64 -0
  16. package/chat/ChatLauncher.js +347 -0
  17. package/chat/ChatProvider.d.ts +53 -0
  18. package/chat/ChatProvider.js +181 -0
  19. package/chat/ChatTabs.d.ts +13 -0
  20. package/chat/ChatTabs.js +24 -0
  21. package/chat/ChatThreads.d.ts +26 -0
  22. package/chat/ChatThreads.js +55 -0
  23. package/chat/ChatWidget.css +1072 -0
  24. package/chat/ChatWidget.d.ts +113 -0
  25. package/chat/ChatWidget.js +495 -0
  26. package/chat/DemoWelcome.d.ts +13 -0
  27. package/chat/DemoWelcome.js +131 -0
  28. package/chat/FollowupSuggestions.d.ts +23 -0
  29. package/chat/FollowupSuggestions.js +34 -0
  30. package/chat/ResponseActions.d.ts +18 -0
  31. package/chat/ResponseActions.js +79 -0
  32. package/chat/TurnContent.d.ts +9 -0
  33. package/chat/TurnContent.js +12 -0
  34. package/chat/context.d.ts +65 -0
  35. package/chat/context.js +27 -0
  36. package/chat/demo-agent-panes.d.ts +5 -0
  37. package/chat/demo-agent-panes.js +49 -0
  38. package/chat/demo-agent.d.ts +3 -0
  39. package/chat/demo-agent.js +28 -0
  40. package/chat/demo-card-components.d.ts +46 -0
  41. package/chat/demo-card-components.js +59 -0
  42. package/chat/demo-cards.d.ts +32 -0
  43. package/chat/demo-cards.js +59 -0
  44. package/chat/demo-fixtures.d.ts +20 -0
  45. package/chat/demo-fixtures.js +113 -0
  46. package/chat/demo-script.d.ts +6 -0
  47. package/chat/demo-script.js +215 -0
  48. package/chat/demo.css +46 -0
  49. package/chat/demo.d.ts +9 -0
  50. package/chat/demo.js +24 -0
  51. package/chat/followups.d.ts +41 -0
  52. package/chat/followups.js +55 -0
  53. package/chat/greeting.d.ts +11 -0
  54. package/chat/greeting.js +21 -0
  55. package/chat/index.d.ts +34 -0
  56. package/chat/index.js +80 -0
  57. package/chat/placement.d.ts +48 -0
  58. package/chat/placement.js +39 -0
  59. package/chat/queries.d.ts +9 -0
  60. package/chat/queries.js +22 -0
  61. package/chat/registry.d.ts +25 -0
  62. package/chat/registry.js +9 -0
  63. package/chat/registry.json.d.ts +69 -0
  64. package/chat/registry.json.js +11 -0
  65. package/chat/server.d.ts +217 -0
  66. package/chat/server.js +488 -0
  67. package/chat/sim.d.ts +71 -0
  68. package/chat/sim.js +125 -0
  69. package/chat/starters.d.ts +15 -0
  70. package/chat/starters.js +38 -0
  71. package/chat/suggestions.d.ts +22 -0
  72. package/chat/suggestions.js +9 -0
  73. package/chat/threads.d.ts +73 -0
  74. package/chat/threads.js +270 -0
  75. package/chat/transport.d.ts +49 -0
  76. package/chat/transport.js +105 -0
  77. package/chat/types.d.ts +210 -0
  78. package/chat/types.js +16 -0
  79. package/chat/upstash.d.ts +63 -0
  80. package/chat/upstash.js +147 -0
  81. package/chat/useChat.d.ts +64 -0
  82. package/chat/useChat.js +241 -0
  83. package/cli/templates/chat/app/api/chat/feedback/route.d.ts +3 -0
  84. package/cli/templates/chat/app/api/chat/followups/route.d.ts +3 -0
  85. package/cli/templates/chat/app/api/chat/guardrails.d.ts +10 -0
  86. package/cli/templates/chat/app/api/chat/route.d.ts +4 -0
  87. package/cli/templates/chat/components/chat/ChatMount.d.ts +1 -0
  88. package/cli/templates/chat/components/chat/models.d.ts +11 -0
  89. package/components/AgentRail/AgentRail.css +2 -2
  90. package/components/Attachment/Attachment.css +14 -0
  91. package/components/Attachment/AttachmentGroup.d.ts +2 -2
  92. package/components/Attachment/AttachmentGroup.js +7 -2
  93. package/components/Composer/Composer.css +88 -4
  94. package/components/Composer/Composer.d.ts +21 -0
  95. package/components/Composer/Composer.js +199 -14
  96. package/components/Mention/Mention.css +195 -0
  97. package/components/Mention/Mention.d.ts +68 -0
  98. package/components/Mention/Mention.js +145 -0
  99. package/components/Mention/mentions.d.ts +81 -0
  100. package/components/Mention/mentions.js +83 -0
  101. package/components/ShaderField/ShaderField.d.ts +6 -5
  102. package/components/ShaderField/useShaderField.d.ts +2 -1
  103. package/components/ShaderField/useShaderField.js +6 -6
  104. package/components/StatusDot/StatusDot.css +36 -13
  105. package/components/StatusDot/StatusDot.d.ts +16 -4
  106. package/components/StatusDot/StatusDot.js +25 -5
  107. package/components/ThreadPanel/ThreadPanel.css +20 -11
  108. package/components/ThreadPanel/ThreadPanel.d.ts +15 -3
  109. package/components/ThreadPanel/ThreadPanel.js +30 -6
  110. package/components/ThreadTabs/ThreadTabs.css +18 -8
  111. package/components/ThreadTabs/ThreadTabs.d.ts +8 -2
  112. package/components/ThreadTabs/ThreadTabs.js +24 -5
  113. package/components/registry.json +9 -1
  114. package/components/registry.json.d.ts +9 -1
  115. package/components/registry.json.js +1 -1
  116. package/fonts/material-symbols.css +2 -1
  117. package/index.d.ts +1 -0
  118. package/index.js +11 -0
  119. package/package.json +43 -2
  120. package/tokens/presets/coral.css +18 -2
  121. package/tokens/presets/forest.css +18 -2
  122. package/tokens/presets/gold.css +18 -2
  123. package/tokens/presets/mono.css +18 -2
  124. package/tokens/presets/pink.css +18 -2
  125. package/tokens/presets/terminal.css +18 -2
  126. package/tokens/presets/violet.css +18 -2
  127. package/tokens/presets/warm.css +18 -2
  128. package/tokens/presets/zest.css +18 -2
  129. package/tokens/registry.json +6 -0
  130. package/tokens/registry.json.d.ts +6 -0
  131. package/tokens/registry.json.js +1 -1
  132. package/tokens/tokens-dark.css +10 -0
  133. package/tokens/tokens-light.css +10 -0
@@ -0,0 +1,82 @@
1
+ "use client";
2
+ import { jsx, jsxs, Fragment } from "react/jsx-runtime";
3
+ import ReactMarkdown from "react-markdown";
4
+ import remarkGfm from "remark-gfm";
5
+ import { AgentStatus } from "../components/AgentStatus/AgentStatus.js";
6
+ import { CodeBlock } from "../components/CodeBlock/CodeBlock.js";
7
+ import { ChatMessage } from "../components/ChatMessage/ChatMessage.js";
8
+ import { Prose } from "../components/Prose/Prose.js";
9
+ import { Reasoning } from "../components/Reasoning/Reasoning.js";
10
+ import { useChatWidget } from "./context.js";
11
+ import { DOCK_QUERY } from "./queries.js";
12
+ import { FollowupSuggestions } from "./FollowupSuggestions.js";
13
+ import { ResponseActions } from "./ResponseActions.js";
14
+ import { TurnCard } from "./TurnContent.js";
15
+ function MarkdownLink({ href, children }) {
16
+ const { view, setOpen, renderLink, isInternalHref } = useChatWidget();
17
+ if (!href || !isInternalHref(href)) return /* @__PURE__ */ jsx(Fragment, { children });
18
+ return /* @__PURE__ */ jsx(Fragment, { children: renderLink({
19
+ href,
20
+ children,
21
+ onNavigate: () => {
22
+ if (view === "full" || !window.matchMedia(DOCK_QUERY).matches) setOpen(false);
23
+ }
24
+ }) });
25
+ }
26
+ const hastText = (node) => node.type === "text" ? node.value ?? "" : (node.children ?? []).map(hastText).join("");
27
+ function MarkdownPre({ node, children, ...props }) {
28
+ const code = node?.children?.find(
29
+ (child) => child.type === "element" && child.tagName === "code"
30
+ );
31
+ if (!code) return /* @__PURE__ */ jsx("pre", { ...props, children });
32
+ const classes = code.properties?.className;
33
+ const language = (Array.isArray(classes) ? classes : []).map(String).find((name) => name.startsWith("language-"))?.slice("language-".length);
34
+ return /* @__PURE__ */ jsx(CodeBlock, { code: hastText(code).replace(/\n$/, ""), language });
35
+ }
36
+ const markdownComponents = {
37
+ a: MarkdownLink,
38
+ pre: MarkdownPre,
39
+ table: ({ children, ...props }) => /* @__PURE__ */ jsx("table", { tabIndex: 0, ...props, children })
40
+ };
41
+ function AssistantTurn({ turn, live }) {
42
+ const thinking = live?.phase === "thinking";
43
+ const tracePoints = live ? live.tracePoints : turn?.tracePoints ?? [];
44
+ const durationSeconds = live ? live.durationSeconds : turn?.durationSeconds;
45
+ const text = live ? live.text : turn?.text ?? "";
46
+ 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
+ return /* @__PURE__ */ jsx(
49
+ ChatMessage,
50
+ {
51
+ role: "assistant",
52
+ showActions: true,
53
+ actions: turn && turn.text !== "" ? /* @__PURE__ */ jsx(ResponseActions, { turn }) : void 0,
54
+ footer: turn?.followups?.length ? /* @__PURE__ */ jsx(
55
+ FollowupSuggestions,
56
+ {
57
+ turnId: turn.id,
58
+ suggestions: turn.followups,
59
+ entrance: turn.id === freshFollowupsId
60
+ }
61
+ ) : followupsPending && turn ? /* @__PURE__ */ jsx(FollowupSuggestions, { turnId: turn.id, suggestions: [], pending: true }) : void 0,
62
+ children: /* @__PURE__ */ jsxs("div", { className: "ds-chat-widget__response", children: [
63
+ (thinking || tracePoints.length > 0) && /* @__PURE__ */ jsx(
64
+ Reasoning,
65
+ {
66
+ streaming: thinking,
67
+ summaryOnly: tracePoints.length === 0,
68
+ duration: durationSeconds,
69
+ summary: thinking && live ? /* @__PURE__ */ jsx(AgentStatus, { state: "working", label: live.statusLabel }) : void 0,
70
+ children: tracePoints.length > 0 && /* @__PURE__ */ jsx("ul", { children: tracePoints.map((point, index) => /* @__PURE__ */ jsx("li", { children: point }, `${index}-${point.slice(0, 24)}`)) })
71
+ }
72
+ ),
73
+ turn?.content,
74
+ turn?.card && /* @__PURE__ */ jsx(TurnCard, { card: turn.card }),
75
+ text !== "" && /* @__PURE__ */ jsx(Prose, { children: /* @__PURE__ */ jsx(ReactMarkdown, { remarkPlugins: [remarkGfm], components: markdownComponents, children: text }) })
76
+ ] })
77
+ }
78
+ );
79
+ }
80
+ export {
81
+ AssistantTurn
82
+ };
@@ -0,0 +1,56 @@
1
+ /* The built-in agent pane. The rail owns its shell; this only says how it
2
+ sits in the widget's bottom sheet, where it fills the width rather than
3
+ holding its own column and the sheet's rounded top edge is the surface. */
4
+ .ds-chat-agent-pane--sheet {
5
+ width: 100%;
6
+ }
7
+
8
+ /* The pane furniture a host's panes commonly carry: a date heading over a
9
+ run of rows, and a small card. Host content, offered as classes rather
10
+ than components so a pane stays whatever the host writes. */
11
+ .ds-chat-agent-pane__group {
12
+ display: flex;
13
+ flex-direction: column;
14
+ gap: var(--gap-100);
15
+ }
16
+
17
+ .ds-chat-agent-pane__group + .ds-chat-agent-pane__group,
18
+ .ds-chat-agent-pane__card + .ds-chat-agent-pane__group {
19
+ margin-top: var(--gap-400);
20
+ }
21
+
22
+ .ds-chat-agent-pane__heading {
23
+ font-family: var(--font-overline-family);
24
+ font-size: var(--font-overline-size);
25
+ font-weight: var(--font-overline-weight);
26
+ line-height: var(--font-overline-line-height);
27
+ letter-spacing: var(--font-overline-letter-spacing);
28
+ text-transform: uppercase;
29
+ color: var(--color-text-tertiary);
30
+ padding: var(--padding-100) var(--padding-200);
31
+ margin: 0;
32
+ }
33
+
34
+ .ds-chat-agent-pane__card {
35
+ display: flex;
36
+ flex-direction: column;
37
+ gap: var(--gap-200);
38
+ padding: var(--padding-300);
39
+ background-color: var(--color-bg-container-secondary);
40
+ border-radius: var(--radius-400);
41
+ }
42
+
43
+ .ds-chat-agent-pane__card-row {
44
+ display: flex;
45
+ align-items: center;
46
+ justify-content: space-between;
47
+ gap: var(--gap-300);
48
+ }
49
+
50
+ .ds-chat-agent-pane__card-label {
51
+ font-family: var(--font-paragraph-sm-emphasis-family);
52
+ font-size: var(--font-paragraph-sm-emphasis-size);
53
+ font-weight: var(--font-paragraph-sm-emphasis-weight);
54
+ line-height: var(--font-paragraph-sm-emphasis-line-height);
55
+ color: var(--color-text-primary);
56
+ }
@@ -0,0 +1,21 @@
1
+ import { AgentRailProfile, AgentRailTab } from '../components/AgentRail/AgentRail';
2
+ export interface ChatAgentPaneProps {
3
+ /** The agent this pane belongs to: the portrait block at the top. */
4
+ profile: AgentRailProfile;
5
+ /** The panes, in display order: activity, approvals, automations, personalisation, or whatever the agent has. */
6
+ tabs: AgentRailTab[];
7
+ /** The tab on stage, when the host owns the state; otherwise the pane keeps its own. */
8
+ activeTab?: string;
9
+ /** Fires with the chosen tab's id. */
10
+ onTabChange?: (id: string) => void;
11
+ /** The widget seated the pane as a bottom sheet, where it fills the width instead of holding its own column. */
12
+ overlay: boolean;
13
+ }
14
+ /**
15
+ * The built-in agent pane: an AgentRail the widget seats on the
16
+ * conversation's trailing edge (inline at 860px and up, a bottom sheet
17
+ * below). A host hands it the agent's profile and panes; the widget owns
18
+ * whether it is up, so `onCollapse` is deliberately unwired here: the
19
+ * header's agent toggle is the one control, both ways.
20
+ */
21
+ export declare function ChatAgentPane({ profile, tabs, activeTab, onTabChange, overlay }: ChatAgentPaneProps): import("react").JSX.Element;
@@ -0,0 +1,25 @@
1
+ "use client";
2
+ import { jsx } from "react/jsx-runtime";
3
+ import { useState } from "react";
4
+ import { AgentRail } from "../components/AgentRail/AgentRail.js";
5
+ import "./ChatAgentPane.css";
6
+ function ChatAgentPane({ profile, tabs, activeTab, onTabChange, overlay }) {
7
+ const [ownTab, setOwnTab] = useState(tabs[0]?.id ?? "");
8
+ const current = activeTab ?? ownTab;
9
+ return /* @__PURE__ */ jsx(
10
+ AgentRail,
11
+ {
12
+ className: overlay ? "ds-chat-agent-pane--sheet" : void 0,
13
+ profile,
14
+ tabs,
15
+ activeTab: current,
16
+ onTabChange: (id) => {
17
+ setOwnTab(id);
18
+ onTabChange?.(id);
19
+ }
20
+ }
21
+ );
22
+ }
23
+ export {
24
+ ChatAgentPane
25
+ };
@@ -0,0 +1,64 @@
1
+ import { ReactNode } from 'react';
2
+ import { AiButtonSummary } from '../components/AiButton/AiButton';
3
+ import { ChatWidgetProps } from './ChatWidget';
4
+ /** The id the panel carries, for a host control's `aria-controls`. */
5
+ export declare const CHAT_PANEL_ID = "ds-chat-panel";
6
+ export interface ChatLauncherProps {
7
+ /** The floating button's text. */
8
+ label?: string;
9
+ /** The button's icon. */
10
+ icon?: string;
11
+ /**
12
+ * A TLDR panel the button summons on hover: a pre-written summary and
13
+ * prompt chips. A tapped chip opens the chat and sends the chip's
14
+ * `prompt`. Hover-capable devices only: the launcher never renders it on
15
+ * touch, and never in server HTML.
16
+ */
17
+ summary?: AiButtonSummary & {
18
+ chips?: {
19
+ id: string;
20
+ label: string;
21
+ prompt: string;
22
+ }[];
23
+ };
24
+ /** Render nothing at all: for routes that carry no chrome. */
25
+ hidden?: boolean;
26
+ /** The panel's DOM id, for a host control's `aria-controls`. */
27
+ panelId?: string;
28
+ /**
29
+ * The attribute the launcher writes on `<html>` to say how the panel sits
30
+ * (`docked`, `docked-left`, `takeover`), so the host's stylesheet can pad
31
+ * the page or hide it. `false` writes none.
32
+ */
33
+ hostAttribute?: string | false;
34
+ /** Accessible name for the panel. */
35
+ panelLabel?: string;
36
+ /** Everything the widget takes. The launcher sets the responsive ones itself. */
37
+ widget?: Omit<ChatWidgetProps, 'fullscreenEnabled' | 'placementEnabled' | 'compact' | 'phone'>;
38
+ /** Rendered inside the panel after the widget: for a host's extra chrome. */
39
+ children?: ReactNode;
40
+ }
41
+ /**
42
+ * The site-wide panel. Mount it once, after the page content, so it sits
43
+ * last in the tab order and never remounts on navigation, which is what
44
+ * lets a mid-stream answer keep streaming while the visitor reads a
45
+ * different page.
46
+ *
47
+ * Docked (wide screens) it is a non-modal complementary region: no scrim,
48
+ * no focus trap, the page stays fully usable beside it. Overlaying (below
49
+ * the threshold, and in fullscreen view) it is modal: scrim, focus trap,
50
+ * body scroll locked, Escape closes.
51
+ *
52
+ * The visitor picks the seat from the header's Chat position menu: either
53
+ * edge for the dock (and for the overlay below the threshold), or floating,
54
+ * a non-modal card that is moved by its header and sized by its edges.
55
+ * Phones get none of it: the takeover is the only form there.
56
+ *
57
+ * The host contract: the launcher sets `hostAttribute` on `<html>` and
58
+ * writes the chosen dock width to `--ds-chat-width` there; a host's
59
+ * stylesheet pads the page beside a docked panel and hides it behind a
60
+ * takeover from those two facts (the package's own stylesheet does not
61
+ * touch the host's body). The panel's geometry reads `--ds-chat-width`,
62
+ * `--ds-chat-inset` and `--ds-chat-float-height`, defaulted on `:root`.
63
+ */
64
+ export declare function ChatLauncher({ label, icon, summary, hidden, panelId, hostAttribute, panelLabel, widget, children, }: ChatLauncherProps): import("react").JSX.Element | null;
@@ -0,0 +1,347 @@
1
+ "use client";
2
+ import { jsx, jsxs, Fragment } from "react/jsx-runtime";
3
+ import { useSyncExternalStore, useLayoutEffect, useRef, useEffect, useState } from "react";
4
+ import { useScrollLock } from "../behaviors/useScrollLock.js";
5
+ import { AiButton } from "../components/AiButton/AiButton.js";
6
+ import { ChatWidget } from "./ChatWidget.js";
7
+ import { useChatWidget } from "./context.js";
8
+ import { isFloatDragStart, FLOAT_EDGE_GAP, FLOAT_MAX_SCALE, dockWidthFromDrag, clampFloatPosition } from "./placement.js";
9
+ import { DOCK_QUERY, TAKEOVER_QUERY } from "./queries.js";
10
+ import "./ChatWidget.css";
11
+ const CHAT_PANEL_ID = "ds-chat-panel";
12
+ const FLOAT_GRIPS = [
13
+ { dir: "n", dx: 0, dy: -1 },
14
+ { dir: "s", dx: 0, dy: 1 },
15
+ { dir: "e", dx: 1, dy: 0 },
16
+ { dir: "w", dx: -1, dy: 0 },
17
+ { dir: "ne", dx: 1, dy: -1 },
18
+ { dir: "nw", dx: -1, dy: -1 },
19
+ { dir: "se", dx: 1, dy: 1 },
20
+ { dir: "sw", dx: -1, dy: 1 }
21
+ ];
22
+ const viewportBounds = () => ({
23
+ left: 0,
24
+ top: 0,
25
+ right: window.innerWidth,
26
+ bottom: window.innerHeight
27
+ });
28
+ const subscribeQuery = (query) => (onChange) => {
29
+ const media = window.matchMedia(query);
30
+ media.addEventListener("change", onChange);
31
+ return () => media.removeEventListener("change", onChange);
32
+ };
33
+ const subscribeDock = subscribeQuery(DOCK_QUERY);
34
+ const readDocked = () => window.matchMedia(DOCK_QUERY).matches;
35
+ const subscribeTakeover = subscribeQuery(TAKEOVER_QUERY);
36
+ const readTakeover = () => window.matchMedia(TAKEOVER_QUERY).matches;
37
+ function ChatLauncher({
38
+ label = "Ask",
39
+ icon = "forum",
40
+ summary,
41
+ hidden = false,
42
+ panelId = CHAT_PANEL_ID,
43
+ hostAttribute = "data-chat",
44
+ panelLabel = "Chat",
45
+ widget,
46
+ children
47
+ }) {
48
+ const { open, setOpen, panelPhase, view, side, floating, returnFocusRef, send } = useChatWidget();
49
+ const launchSuggestion = (id) => {
50
+ const chip = summary?.chips?.find((c) => c.id === id);
51
+ if (!chip) return;
52
+ setOpen(true);
53
+ send(chip.prompt);
54
+ };
55
+ const docked = useSyncExternalStore(subscribeDock, readDocked, () => false);
56
+ const takeover = useSyncExternalStore(subscribeTakeover, readTakeover, () => false);
57
+ const isFull = view === "full" || takeover;
58
+ const isFloating = floating && !isFull;
59
+ const isDocked = docked && !isFull && !isFloating;
60
+ const modal = open && !hidden && (isFull || !docked && !isFloating);
61
+ const showPanel = open && !hidden;
62
+ const renderPanel = (open || panelPhase === "closing") && !hidden;
63
+ useLayoutEffect(() => {
64
+ if (!hostAttribute) return;
65
+ const root = document.documentElement;
66
+ if (showPanel && takeover) {
67
+ const scrollY = window.scrollY;
68
+ root.setAttribute(hostAttribute, "takeover");
69
+ window.scrollTo({ top: 0, behavior: "instant" });
70
+ return () => {
71
+ root.removeAttribute(hostAttribute);
72
+ window.scrollTo({ top: scrollY, behavior: "instant" });
73
+ };
74
+ }
75
+ if (showPanel && isDocked) {
76
+ root.setAttribute(hostAttribute, side === "left" ? "docked-left" : "docked");
77
+ return () => root.removeAttribute(hostAttribute);
78
+ }
79
+ root.removeAttribute(hostAttribute);
80
+ }, [showPanel, isDocked, side, takeover, hostAttribute]);
81
+ useScrollLock(modal && !takeover);
82
+ const panelRef = useRef(null);
83
+ const wasOpen = useRef(false);
84
+ useEffect(() => {
85
+ if (wasOpen.current && !open) {
86
+ const target = returnFocusRef.current;
87
+ if (target && target.isConnected) target.focus();
88
+ }
89
+ wasOpen.current = open;
90
+ }, [open, returnFocusRef]);
91
+ const dragRef = useRef(null);
92
+ const [resizing, setResizing] = useState(false);
93
+ const beginResize = (e) => {
94
+ if (e.pointerType === "mouse" && e.button !== 0 || dragRef.current) return;
95
+ const rect = panelRef.current?.getBoundingClientRect();
96
+ if (!rect) return;
97
+ dragRef.current = { startX: e.clientX, width: rect.width };
98
+ e.currentTarget.setPointerCapture(e.pointerId);
99
+ setResizing(true);
100
+ document.documentElement.setAttribute("data-chat-resizing", "");
101
+ };
102
+ const moveResize = (e) => {
103
+ const drag = dragRef.current;
104
+ if (!drag) return;
105
+ const width = dockWidthFromDrag(drag.width, e.clientX - drag.startX, side);
106
+ document.documentElement.style.setProperty("--ds-chat-width", `${width}px`);
107
+ };
108
+ const endResize = () => {
109
+ dragRef.current = null;
110
+ setResizing(false);
111
+ document.documentElement.removeAttribute("data-chat-resizing");
112
+ };
113
+ const [floatPos, setFloatPos] = useState(null);
114
+ const moveRef = useRef(null);
115
+ const clampFloat = (x, y, rect) => clampFloatPosition(x, y, rect, viewportBounds());
116
+ const beginMove = (e) => {
117
+ if (!isFloating || moveRef.current) return;
118
+ if (e.pointerType === "mouse" && e.button !== 0) return;
119
+ if (!isFloatDragStart(e.target)) return;
120
+ const rect = e.currentTarget.getBoundingClientRect();
121
+ moveRef.current = { dx: e.clientX - rect.left, dy: e.clientY - rect.top, x: rect.left, y: rect.top };
122
+ try {
123
+ e.currentTarget.setPointerCapture(e.pointerId);
124
+ } catch {
125
+ }
126
+ document.documentElement.setAttribute("data-chat-resizing", "");
127
+ };
128
+ const moveMove = (e) => {
129
+ const move = moveRef.current;
130
+ if (!move) return;
131
+ const node = e.currentTarget;
132
+ const next = clampFloat(e.clientX - move.dx, e.clientY - move.dy, node.getBoundingClientRect());
133
+ move.x = next.x;
134
+ move.y = next.y;
135
+ node.style.setProperty("--ds-chat-float-x", `${next.x}px`);
136
+ node.style.setProperty("--ds-chat-float-y", `${next.y}px`);
137
+ };
138
+ const endMove = () => {
139
+ const move = moveRef.current;
140
+ if (!move) return;
141
+ moveRef.current = null;
142
+ setFloatPos({ x: move.x, y: move.y });
143
+ document.documentElement.removeAttribute("data-chat-resizing");
144
+ };
145
+ const [floatSize, setFloatSize] = useState(null);
146
+ const sizeRef = useRef(null);
147
+ const beginSize = (e, dx, dy) => {
148
+ if (e.pointerType === "mouse" && e.button !== 0 || sizeRef.current) return;
149
+ const rect = panelRef.current?.getBoundingClientRect();
150
+ if (!rect) return;
151
+ e.stopPropagation();
152
+ const root = getComputedStyle(document.documentElement);
153
+ const inset = parseFloat(root.getPropertyValue("--ds-chat-inset")) || 0;
154
+ const minW = parseFloat(root.getPropertyValue("--ds-chat-width")) || rect.width;
155
+ const restH = parseFloat(root.getPropertyValue("--ds-chat-float-height")) || rect.height;
156
+ const minH = Math.min(restH, window.innerHeight - inset * 2);
157
+ sizeRef.current = {
158
+ dx,
159
+ dy,
160
+ startX: e.clientX,
161
+ startY: e.clientY,
162
+ rect,
163
+ minW,
164
+ minH,
165
+ next: { x: rect.left, y: rect.top, w: rect.width, h: rect.height }
166
+ };
167
+ try {
168
+ e.currentTarget.setPointerCapture(e.pointerId);
169
+ } catch {
170
+ }
171
+ document.documentElement.setAttribute("data-chat-resizing", "");
172
+ };
173
+ const moveSize = (e) => {
174
+ const drag = sizeRef.current;
175
+ const node = panelRef.current;
176
+ if (!drag || !node) return;
177
+ const { rect, next } = drag;
178
+ if (drag.dx !== 0) {
179
+ const room = drag.dx > 0 ? window.innerWidth - FLOAT_EDGE_GAP - rect.left : rect.right - FLOAT_EDGE_GAP;
180
+ const max = Math.max(drag.minW, Math.min(drag.minW * FLOAT_MAX_SCALE, room));
181
+ next.w = Math.round(
182
+ Math.min(max, Math.max(drag.minW, rect.width + drag.dx * (e.clientX - drag.startX)))
183
+ );
184
+ next.x = Math.round(drag.dx > 0 ? rect.left : rect.right - next.w);
185
+ }
186
+ if (drag.dy !== 0) {
187
+ const room = drag.dy > 0 ? window.innerHeight - FLOAT_EDGE_GAP - rect.top : rect.bottom - FLOAT_EDGE_GAP;
188
+ const max = Math.max(drag.minH, Math.min(drag.minH * FLOAT_MAX_SCALE, room));
189
+ next.h = Math.round(
190
+ Math.min(max, Math.max(drag.minH, rect.height + drag.dy * (e.clientY - drag.startY)))
191
+ );
192
+ next.y = Math.round(drag.dy > 0 ? rect.top : rect.bottom - next.h);
193
+ }
194
+ node.style.setProperty("--ds-chat-float-x", `${next.x}px`);
195
+ node.style.setProperty("--ds-chat-float-y", `${next.y}px`);
196
+ node.style.setProperty("--ds-chat-float-w", `${next.w}px`);
197
+ node.style.setProperty("--ds-chat-float-h", `${next.h}px`);
198
+ };
199
+ const endSize = () => {
200
+ const drag = sizeRef.current;
201
+ if (!drag) return;
202
+ sizeRef.current = null;
203
+ setFloatPos({ x: drag.next.x, y: drag.next.y });
204
+ setFloatSize({ w: drag.next.w, h: drag.next.h });
205
+ document.documentElement.removeAttribute("data-chat-resizing");
206
+ };
207
+ useEffect(() => {
208
+ if (!isFloating || !floatPos) return;
209
+ const onResize = () => {
210
+ const rect = panelRef.current?.getBoundingClientRect();
211
+ if (!rect) return;
212
+ setFloatPos((pos) => {
213
+ if (!pos) return pos;
214
+ const next = clampFloat(pos.x, pos.y, rect);
215
+ return next.x === pos.x && next.y === pos.y ? pos : next;
216
+ });
217
+ };
218
+ window.addEventListener("resize", onResize);
219
+ return () => window.removeEventListener("resize", onResize);
220
+ }, [isFloating, floatPos]);
221
+ if (hidden) return null;
222
+ if (!renderPanel) {
223
+ return /* @__PURE__ */ jsx("div", { className: "ds-chat-launcher__fab", children: /* @__PURE__ */ jsx(
224
+ AiButton,
225
+ {
226
+ label,
227
+ icon,
228
+ "aria-expanded": false,
229
+ "aria-controls": panelId,
230
+ summary: summary ? {
231
+ overline: summary.overline,
232
+ title: summary.title,
233
+ caption: summary.caption,
234
+ text: summary.text,
235
+ suggestions: summary.suggestions ?? summary.chips?.map(({ id, label: chipLabel }) => ({ id, label: chipLabel }))
236
+ } : void 0,
237
+ onSummarySuggestion: launchSuggestion,
238
+ onClick: (event) => {
239
+ returnFocusRef.current = event.currentTarget;
240
+ setOpen(true);
241
+ }
242
+ }
243
+ ) });
244
+ }
245
+ const handleKeyDown = (event) => {
246
+ if (event.key === "Escape") {
247
+ setOpen(false);
248
+ return;
249
+ }
250
+ if (modal && event.key === "Tab" && panelRef.current) {
251
+ const focusables = panelRef.current.querySelectorAll(
252
+ 'button:not([disabled]), a[href], textarea:not([disabled]), input:not([disabled]), [tabindex]:not([tabindex="-1"])'
253
+ );
254
+ if (focusables.length === 0) return;
255
+ const first = focusables[0];
256
+ const last = focusables[focusables.length - 1];
257
+ if (event.shiftKey && document.activeElement === first) {
258
+ event.preventDefault();
259
+ last.focus();
260
+ } else if (!event.shiftKey && document.activeElement === last) {
261
+ event.preventDefault();
262
+ first.focus();
263
+ }
264
+ }
265
+ };
266
+ const panelClass = [
267
+ "ds-chat-launcher__panel",
268
+ side === "left" ? "ds-chat-launcher__panel--left" : "",
269
+ isFloating ? "ds-chat-launcher__panel--floating" : "",
270
+ isFull ? "ds-chat-launcher__panel--full" : "",
271
+ panelPhase === "closing" ? "ds-chat-launcher__panel--closing" : ""
272
+ ].filter(Boolean).join(" ");
273
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
274
+ modal && !isFull && /* @__PURE__ */ jsx("div", { className: "ds-chat-launcher__scrim", onClick: () => setOpen(false), "aria-hidden": "true" }),
275
+ /* @__PURE__ */ jsxs(
276
+ "div",
277
+ {
278
+ ref: panelRef,
279
+ id: panelId,
280
+ className: panelClass,
281
+ style: isFloating && (floatPos || floatSize) ? {
282
+ ...floatPos && {
283
+ "--ds-chat-float-x": `${floatPos.x}px`,
284
+ "--ds-chat-float-y": `${floatPos.y}px`
285
+ },
286
+ ...floatSize && {
287
+ "--ds-chat-float-w": `${floatSize.w}px`,
288
+ "--ds-chat-float-h": `${floatSize.h}px`
289
+ }
290
+ } : void 0,
291
+ onPointerDown: beginMove,
292
+ onPointerMove: moveMove,
293
+ onPointerUp: endMove,
294
+ onPointerCancel: endMove,
295
+ role: modal ? "dialog" : "complementary",
296
+ "aria-modal": modal || void 0,
297
+ "aria-label": panelLabel,
298
+ onKeyDown: handleKeyDown,
299
+ children: [
300
+ /* @__PURE__ */ jsx(
301
+ ChatWidget,
302
+ {
303
+ ...widget,
304
+ fullscreenEnabled: !takeover,
305
+ placementEnabled: !takeover,
306
+ compact: takeover || !isFull,
307
+ phone: takeover
308
+ }
309
+ ),
310
+ children,
311
+ isFloating && FLOAT_GRIPS.map(({ dir, dx, dy }) => /* @__PURE__ */ jsx(
312
+ "div",
313
+ {
314
+ className: "ds-chat-launcher__float-grip",
315
+ "data-dir": dir,
316
+ "aria-hidden": "true",
317
+ onPointerDown: (e) => beginSize(e, dx, dy),
318
+ onPointerMove: moveSize,
319
+ onPointerUp: endSize,
320
+ onPointerCancel: endSize
321
+ },
322
+ dir
323
+ ))
324
+ ]
325
+ }
326
+ ),
327
+ showPanel && isDocked && /* @__PURE__ */ jsx(
328
+ "div",
329
+ {
330
+ className: [
331
+ "ds-chat-launcher__dock-handle",
332
+ side === "left" ? "ds-chat-launcher__dock-handle--left" : "",
333
+ resizing ? "ds-chat-launcher__dock-handle--resizing" : ""
334
+ ].filter(Boolean).join(" "),
335
+ "aria-hidden": "true",
336
+ onPointerDown: beginResize,
337
+ onPointerMove: moveResize,
338
+ onPointerUp: endResize,
339
+ onPointerCancel: endResize
340
+ }
341
+ )
342
+ ] });
343
+ }
344
+ export {
345
+ CHAT_PANEL_ID,
346
+ ChatLauncher
347
+ };
@@ -0,0 +1,53 @@
1
+ import { ReactNode } from 'react';
2
+ import { ChatCardRenderers, ChatLinkRenderer, FeedbackSink } from './context';
3
+ import { FollowupsSource } from './followups';
4
+ import { ChatPlacement } from './placement';
5
+ import { ThreadsOptions } from './threads';
6
+ import { ChatTransport } from './types';
7
+ import { ChatPlaceholderCopy } from './useChat';
8
+ export interface ChatProviderProps {
9
+ /**
10
+ * Where the conversation goes. Defaults to the fetch transport against
11
+ * `endpoint`, carrying the visitor's model pick. A script from
12
+ * `scriptChat()` runs the widget with no backend at all.
13
+ */
14
+ transport?: ChatTransport;
15
+ /** The endpoint the default transport posts to. */
16
+ endpoint?: string;
17
+ /** The seat the panel opens in. */
18
+ defaultPlacement?: ChatPlacement;
19
+ /**
20
+ * Where follow-up questions come from after an answer commits: a source
21
+ * function, an endpoint for the built-in fetcher, or `false` for none.
22
+ * Defaults to the fetcher against `/api/chat/followups`.
23
+ */
24
+ followups?: FollowupsSource | string | false;
25
+ /** The written questions offered when the source returns none. */
26
+ fallbackFollowups?: readonly string[];
27
+ /**
28
+ * Where a thumbs verdict goes: a sink function, an endpoint for the
29
+ * built-in poster, or `false` to hide the thumbs. Defaults to the poster
30
+ * against `/api/chat/feedback`. Thumbs only ever show on a turn the
31
+ * server logged (one with an exchange id).
32
+ */
33
+ feedback?: FeedbackSink | string | false;
34
+ /** Draws `card` events by kind. */
35
+ renderers?: ChatCardRenderers;
36
+ /** Renders a same-site link in an answer. The default is a plain anchor; a Next.js host passes its Link. */
37
+ renderLink?: ChatLinkRenderer;
38
+ /** Which hrefs in an answer become links. Everything else renders as text. */
39
+ isInternalHref?: (href: string) => boolean;
40
+ /** Seeds the thread lifecycle behind the `threads` and `tabs` slots. */
41
+ threads?: ThreadsOptions;
42
+ /** Replacement copy for the hook's placeholder replies. */
43
+ placeholders?: Partial<ChatPlaceholderCopy>;
44
+ children: ReactNode;
45
+ }
46
+ /**
47
+ * The chat's whole client state. Mount it once, high in the tree: a root
48
+ * layout never remounts on client-side navigation, so the transcript, a
49
+ * stream in flight, the half-typed draft and the open state survive
50
+ * clicking between pages for free. A hard reload starts clean by design:
51
+ * no storage, no persistence.
52
+ */
53
+ export declare function ChatProvider({ transport, endpoint, defaultPlacement, followups, fallbackFollowups, feedback, renderers, renderLink, isInternalHref, threads: threadsOptions, placeholders, children, }: ChatProviderProps): import("react").JSX.Element;