rift-ds 1.7.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.
- package/README.md +28 -5
- package/bin/rift-ds.mjs +142 -29
- package/bin/templates/chat/.env.chat.example +21 -0
- package/bin/templates/chat/app/api/chat/feedback/route.ts +12 -0
- package/bin/templates/chat/app/api/chat/followups/route.ts +23 -0
- package/bin/templates/chat/app/api/chat/guardrails.ts +18 -0
- package/bin/templates/chat/app/api/chat/route.ts +31 -0
- package/bin/templates/chat/components/chat/ChatMount.tsx +52 -0
- package/bin/templates/chat/components/chat/models.ts +29 -0
- package/chat/AssistantTurn.d.ts +5 -0
- package/chat/AssistantTurn.js +82 -0
- package/chat/ChatAgentPane.css +56 -0
- package/chat/ChatAgentPane.d.ts +21 -0
- package/chat/ChatAgentPane.js +25 -0
- package/chat/ChatLauncher.d.ts +64 -0
- package/chat/ChatLauncher.js +347 -0
- package/chat/ChatProvider.d.ts +53 -0
- package/chat/ChatProvider.js +181 -0
- package/chat/ChatTabs.d.ts +13 -0
- package/chat/ChatTabs.js +24 -0
- package/chat/ChatThreads.d.ts +26 -0
- package/chat/ChatThreads.js +55 -0
- package/chat/ChatWidget.css +1072 -0
- package/chat/ChatWidget.d.ts +113 -0
- package/chat/ChatWidget.js +495 -0
- package/chat/DemoWelcome.d.ts +13 -0
- package/chat/DemoWelcome.js +131 -0
- package/chat/FollowupSuggestions.d.ts +23 -0
- package/chat/FollowupSuggestions.js +34 -0
- package/chat/ResponseActions.d.ts +18 -0
- package/chat/ResponseActions.js +79 -0
- package/chat/TurnContent.d.ts +9 -0
- package/chat/TurnContent.js +12 -0
- package/chat/context.d.ts +65 -0
- package/chat/context.js +27 -0
- package/chat/demo-agent-panes.d.ts +5 -0
- package/chat/demo-agent-panes.js +49 -0
- package/chat/demo-agent.d.ts +3 -0
- package/chat/demo-agent.js +28 -0
- package/chat/demo-card-components.d.ts +46 -0
- package/chat/demo-card-components.js +59 -0
- package/chat/demo-cards.d.ts +32 -0
- package/chat/demo-cards.js +59 -0
- package/chat/demo-fixtures.d.ts +20 -0
- package/chat/demo-fixtures.js +113 -0
- package/chat/demo-script.d.ts +6 -0
- package/chat/demo-script.js +215 -0
- package/chat/demo.css +46 -0
- package/chat/demo.d.ts +9 -0
- package/chat/demo.js +24 -0
- package/chat/followups.d.ts +41 -0
- package/chat/followups.js +55 -0
- package/chat/greeting.d.ts +11 -0
- package/chat/greeting.js +21 -0
- package/chat/index.d.ts +34 -0
- package/chat/index.js +80 -0
- package/chat/placement.d.ts +48 -0
- package/chat/placement.js +39 -0
- package/chat/queries.d.ts +9 -0
- package/chat/queries.js +22 -0
- package/chat/registry.d.ts +25 -0
- package/chat/registry.js +9 -0
- package/chat/registry.json.d.ts +69 -0
- package/chat/registry.json.js +11 -0
- package/chat/server.d.ts +217 -0
- package/chat/server.js +488 -0
- package/chat/sim.d.ts +71 -0
- package/chat/sim.js +125 -0
- package/chat/starters.d.ts +15 -0
- package/chat/starters.js +38 -0
- package/chat/suggestions.d.ts +22 -0
- package/chat/suggestions.js +9 -0
- package/chat/threads.d.ts +73 -0
- package/chat/threads.js +270 -0
- package/chat/transport.d.ts +49 -0
- package/chat/transport.js +105 -0
- package/chat/types.d.ts +210 -0
- package/chat/types.js +16 -0
- package/chat/upstash.d.ts +63 -0
- package/chat/upstash.js +147 -0
- package/chat/useChat.d.ts +64 -0
- package/chat/useChat.js +241 -0
- package/cli/templates/chat/app/api/chat/feedback/route.d.ts +3 -0
- package/cli/templates/chat/app/api/chat/followups/route.d.ts +3 -0
- package/cli/templates/chat/app/api/chat/guardrails.d.ts +10 -0
- package/cli/templates/chat/app/api/chat/route.d.ts +4 -0
- package/cli/templates/chat/components/chat/ChatMount.d.ts +1 -0
- package/cli/templates/chat/components/chat/models.d.ts +11 -0
- package/components/ShaderField/ShaderField.d.ts +3 -2
- package/components/ShaderField/useShaderField.d.ts +2 -1
- package/components/ShaderField/useShaderField.js +6 -6
- package/fonts/material-symbols.css +2 -1
- package/package.json +39 -2
- package/tokens/presets/coral.css +18 -2
- package/tokens/presets/forest.css +18 -2
- package/tokens/presets/gold.css +18 -2
- package/tokens/presets/mono.css +18 -2
- package/tokens/presets/pink.css +18 -2
- package/tokens/presets/terminal.css +18 -2
- package/tokens/presets/violet.css +18 -2
- package/tokens/presets/warm.css +18 -2
- package/tokens/presets/zest.css +18 -2
- package/tokens/registry.json +6 -0
- package/tokens/registry.json.d.ts +6 -0
- package/tokens/registry.json.js +1 -1
- package/tokens/tokens-dark.css +10 -0
- package/tokens/tokens-light.css +10 -0
|
@@ -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;
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx } from "react/jsx-runtime";
|
|
3
|
+
import { useState, useMemo, useRef, useEffect, useCallback } from "react";
|
|
4
|
+
import { MOTION_EXIT_SYNC_MS } from "../tokens/motion.js";
|
|
5
|
+
import { createFeedbackPoster, isInternalPath, ChatWidgetContext } from "./context.js";
|
|
6
|
+
import { createFollowupsFetcher, followupTarget, DEFAULT_FALLBACK_FOLLOWUPS, pickFallbackFollowups } from "./followups.js";
|
|
7
|
+
import { useThreads } from "./threads.js";
|
|
8
|
+
import { createFetchTransport } from "./transport.js";
|
|
9
|
+
import { DEFAULT_PLACEHOLDER_COPY, useChat } from "./useChat.js";
|
|
10
|
+
const defaultRenderLink = ({ href, children, onNavigate }) => /* @__PURE__ */ jsx("a", { href, onClick: onNavigate, children });
|
|
11
|
+
function ChatProvider({
|
|
12
|
+
transport,
|
|
13
|
+
endpoint = "/api/chat",
|
|
14
|
+
defaultPlacement = "right",
|
|
15
|
+
followups = "/api/chat/followups",
|
|
16
|
+
fallbackFollowups = DEFAULT_FALLBACK_FOLLOWUPS,
|
|
17
|
+
feedback = "/api/chat/feedback",
|
|
18
|
+
renderers,
|
|
19
|
+
renderLink = defaultRenderLink,
|
|
20
|
+
isInternalHref = isInternalPath,
|
|
21
|
+
threads: threadsOptions,
|
|
22
|
+
placeholders,
|
|
23
|
+
children
|
|
24
|
+
}) {
|
|
25
|
+
const [chosenModel, setChosenModel] = useState(null);
|
|
26
|
+
const fallbackTransport = useMemo(
|
|
27
|
+
() => createFetchTransport(endpoint, { getModel: () => chosenModel }),
|
|
28
|
+
[endpoint, chosenModel]
|
|
29
|
+
);
|
|
30
|
+
const placeholderCopy = useMemo(
|
|
31
|
+
() => ({ ...DEFAULT_PLACEHOLDER_COPY, ...placeholders }),
|
|
32
|
+
[placeholders]
|
|
33
|
+
);
|
|
34
|
+
const chat = useChat(transport ?? fallbackTransport, placeholderCopy);
|
|
35
|
+
const threads = useThreads(chat, threadsOptions);
|
|
36
|
+
const followupsSource = useMemo(() => {
|
|
37
|
+
if (followups === false) return null;
|
|
38
|
+
return typeof followups === "string" ? createFollowupsFetcher(followups) : followups;
|
|
39
|
+
}, [followups]);
|
|
40
|
+
const { setTurnFollowups } = chat;
|
|
41
|
+
const target = useMemo(() => followupTarget(chat.turns), [chat.turns]);
|
|
42
|
+
const askedRef = useRef(/* @__PURE__ */ new Set());
|
|
43
|
+
const [freshFollowupsId, setFreshFollowupsId] = useState(null);
|
|
44
|
+
useEffect(() => {
|
|
45
|
+
if (!target || askedRef.current.has(target.id)) return;
|
|
46
|
+
askedRef.current.add(target.id);
|
|
47
|
+
const offer = (suggestions) => {
|
|
48
|
+
const offered = suggestions.length > 0 ? suggestions : pickFallbackFollowups(fallbackFollowups, target.id, target.question);
|
|
49
|
+
setTurnFollowups(target.id, offered);
|
|
50
|
+
setFreshFollowupsId(target.id);
|
|
51
|
+
};
|
|
52
|
+
if (!followupsSource) {
|
|
53
|
+
offer([]);
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
void followupsSource(target.question, target.answer, AbortSignal.timeout(1e4)).catch(() => []).then(offer);
|
|
57
|
+
}, [target, setTurnFollowups, followupsSource, fallbackFollowups]);
|
|
58
|
+
const feedbackSink = useMemo(() => {
|
|
59
|
+
if (feedback === false) return null;
|
|
60
|
+
return typeof feedback === "string" ? createFeedbackPoster(feedback) : feedback;
|
|
61
|
+
}, [feedback]);
|
|
62
|
+
const { setTurnFeedback } = chat;
|
|
63
|
+
const submitFeedback = useMemo(
|
|
64
|
+
() => feedbackSink ? (turnId, exchangeId, verdict) => {
|
|
65
|
+
setTurnFeedback(turnId, verdict);
|
|
66
|
+
void feedbackSink(exchangeId, verdict).catch(() => {
|
|
67
|
+
});
|
|
68
|
+
} : null,
|
|
69
|
+
[feedbackSink, setTurnFeedback]
|
|
70
|
+
);
|
|
71
|
+
const [starterRevealKey, setStarterRevealKey] = useState(null);
|
|
72
|
+
const [starterSeed, setStarterSeed] = useState(() => Math.random());
|
|
73
|
+
const { reset: resetChat } = chat;
|
|
74
|
+
const reset = useCallback(() => {
|
|
75
|
+
resetChat();
|
|
76
|
+
setStarterSeed(Math.random());
|
|
77
|
+
setStarterRevealKey(null);
|
|
78
|
+
}, [resetChat]);
|
|
79
|
+
const [open, setOpenState] = useState(false);
|
|
80
|
+
const [panelPhase, setPanelPhase] = useState("closed");
|
|
81
|
+
const closeTimer = useRef(null);
|
|
82
|
+
useEffect(
|
|
83
|
+
() => () => {
|
|
84
|
+
if (closeTimer.current) clearTimeout(closeTimer.current);
|
|
85
|
+
},
|
|
86
|
+
[]
|
|
87
|
+
);
|
|
88
|
+
const setOpen = useCallback((next) => {
|
|
89
|
+
if (next) {
|
|
90
|
+
if (closeTimer.current) {
|
|
91
|
+
clearTimeout(closeTimer.current);
|
|
92
|
+
closeTimer.current = null;
|
|
93
|
+
}
|
|
94
|
+
setPanelPhase("open");
|
|
95
|
+
setOpenState(true);
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
setFreshFollowupsId(null);
|
|
99
|
+
setOpenState(false);
|
|
100
|
+
setPanelPhase((phase) => phase === "open" ? "closing" : phase);
|
|
101
|
+
closeTimer.current = setTimeout(() => {
|
|
102
|
+
closeTimer.current = null;
|
|
103
|
+
setPanelPhase("closed");
|
|
104
|
+
}, MOTION_EXIT_SYNC_MS);
|
|
105
|
+
}, []);
|
|
106
|
+
const [view, setView] = useState("panel");
|
|
107
|
+
const [side, setSide] = useState(
|
|
108
|
+
defaultPlacement === "floating" ? "right" : defaultPlacement
|
|
109
|
+
);
|
|
110
|
+
const [floating, setFloating] = useState(defaultPlacement === "floating");
|
|
111
|
+
const placement = floating ? "floating" : side;
|
|
112
|
+
const setPlacement = useCallback((next) => {
|
|
113
|
+
if (next === "floating") {
|
|
114
|
+
setFloating(true);
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
setFloating(false);
|
|
118
|
+
setSide(next);
|
|
119
|
+
}, []);
|
|
120
|
+
const [draft, setDraft] = useState("");
|
|
121
|
+
const returnFocusRef = useRef(null);
|
|
122
|
+
const toggleOpen = useCallback(() => setOpen(!open), [open, setOpen]);
|
|
123
|
+
const renderersValue = useMemo(() => renderers ?? {}, [renderers]);
|
|
124
|
+
const value = useMemo(
|
|
125
|
+
() => ({
|
|
126
|
+
...chat,
|
|
127
|
+
reset,
|
|
128
|
+
chosenModel,
|
|
129
|
+
setChosenModel,
|
|
130
|
+
open,
|
|
131
|
+
setOpen,
|
|
132
|
+
toggleOpen,
|
|
133
|
+
panelPhase,
|
|
134
|
+
starterRevealKey,
|
|
135
|
+
setStarterRevealKey,
|
|
136
|
+
starterSeed,
|
|
137
|
+
freshFollowupsId,
|
|
138
|
+
view,
|
|
139
|
+
setView,
|
|
140
|
+
placement,
|
|
141
|
+
setPlacement,
|
|
142
|
+
side,
|
|
143
|
+
floating,
|
|
144
|
+
draft,
|
|
145
|
+
setDraft,
|
|
146
|
+
returnFocusRef,
|
|
147
|
+
threads,
|
|
148
|
+
renderers: renderersValue,
|
|
149
|
+
renderLink,
|
|
150
|
+
isInternalHref,
|
|
151
|
+
submitFeedback
|
|
152
|
+
}),
|
|
153
|
+
[
|
|
154
|
+
chat,
|
|
155
|
+
reset,
|
|
156
|
+
chosenModel,
|
|
157
|
+
open,
|
|
158
|
+
setOpen,
|
|
159
|
+
toggleOpen,
|
|
160
|
+
panelPhase,
|
|
161
|
+
starterRevealKey,
|
|
162
|
+
starterSeed,
|
|
163
|
+
freshFollowupsId,
|
|
164
|
+
view,
|
|
165
|
+
placement,
|
|
166
|
+
setPlacement,
|
|
167
|
+
side,
|
|
168
|
+
floating,
|
|
169
|
+
draft,
|
|
170
|
+
threads,
|
|
171
|
+
renderersValue,
|
|
172
|
+
renderLink,
|
|
173
|
+
isInternalHref,
|
|
174
|
+
submitFeedback
|
|
175
|
+
]
|
|
176
|
+
);
|
|
177
|
+
return /* @__PURE__ */ jsx(ChatWidgetContext.Provider, { value, children });
|
|
178
|
+
}
|
|
179
|
+
export {
|
|
180
|
+
ChatProvider
|
|
181
|
+
};
|