cherry-styled-components 0.2.11 → 0.2.13

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 (47) hide show
  1. package/README.md +89 -28
  2. package/dist/avatar-dropzone.js +8 -8
  3. package/dist/avatar.d.ts +17 -0
  4. package/dist/avatar.js +68 -0
  5. package/dist/button.js +15 -12
  6. package/dist/callout.d.ts +13 -0
  7. package/dist/callout.js +121 -0
  8. package/dist/chat-input.d.ts +18 -0
  9. package/dist/chat-input.js +257 -0
  10. package/dist/chat-launcher.d.ts +10 -0
  11. package/dist/chat-launcher.js +78 -0
  12. package/dist/chat-message-list.d.ts +12 -0
  13. package/dist/chat-message-list.js +88 -0
  14. package/dist/chat-message.d.ts +16 -0
  15. package/dist/chat-message.js +56 -0
  16. package/dist/chat-panel.d.ts +25 -0
  17. package/dist/chat-panel.js +163 -0
  18. package/dist/chat-provider.d.ts +86 -0
  19. package/dist/chat-provider.js +295 -0
  20. package/dist/chat-showcase.d.ts +18 -0
  21. package/dist/chat-showcase.js +234 -0
  22. package/dist/chat-sources.d.ts +10 -0
  23. package/dist/chat-sources.js +41 -0
  24. package/dist/chat-typing.d.ts +7 -0
  25. package/dist/chat-typing.js +44 -0
  26. package/dist/dropzone.js +14 -14
  27. package/dist/icon-button.js +7 -7
  28. package/dist/index.d.ts +13 -0
  29. package/dist/index.js +18 -3
  30. package/dist/input.js +6 -6
  31. package/dist/modal.js +6 -6
  32. package/dist/prose.d.ts +17 -0
  33. package/dist/prose.js +75 -0
  34. package/dist/spinner.d.ts +11 -0
  35. package/dist/spinner.js +21 -0
  36. package/dist/styled-components/client-theme-provider.js +13 -3
  37. package/dist/tabs.js +6 -7
  38. package/dist/theme-toggle.js +5 -5
  39. package/dist/toast.d.ts +2 -2
  40. package/dist/utils/index.d.ts +2 -0
  41. package/dist/utils/mixins.d.ts +30 -0
  42. package/dist/utils/mixins.js +24 -3
  43. package/dist/utils/use-lock-body-scroll.d.ts +5 -0
  44. package/dist/utils/use-lock-body-scroll.js +25 -0
  45. package/dist/utils/use-media-query.d.ts +14 -0
  46. package/dist/utils/use-media-query.js +28 -0
  47. package/package.json +15 -14
@@ -0,0 +1,163 @@
1
+ "use client";
2
+ "use client";
3
+ import { mq } from "./utils/theme.js";
4
+ import { useLockBodyScroll } from "./utils/use-lock-body-scroll.js";
5
+ import { useBelowBreakpoint } from "./utils/use-media-query.js";
6
+ import { Icon } from "./icon.js";
7
+ import { ChatContext } from "./chat-provider.js";
8
+ import { IconButton } from "./icon-button.js";
9
+ import { jsx, jsxs } from "react/jsx-runtime";
10
+ import { forwardRef, useContext, useEffect, useRef } from "react";
11
+ import styled, { css } from "styled-components";
12
+ //#region src/lib/chat-panel.tsx
13
+ var DEFAULT_WIDTH = 420;
14
+ var FOCUSABLE_SELECTOR = "a[href], button:not([disabled]), input:not([disabled]), textarea:not([disabled]), select:not([disabled]), [tabindex]:not([tabindex=\"-1\"])";
15
+ var StyledChatPanel = styled.div.withConfig({
16
+ displayName: "chat-panel__StyledChatPanel",
17
+ componentId: "sc-701cc462-0"
18
+ })([
19
+ `display:flex;flex-direction:column;background:`,
20
+ `;color:`,
21
+ `;& .chat-header{display:flex;flex-wrap:nowrap;justify-content:space-between;align-items:center;gap:12px;min-height:62px;padding:12px 20px;border-bottom:solid 1px `,
22
+ `;flex-shrink:0;}& .chat-header-title{display:flex;align-items:center;gap:12px;color:`,
23
+ `;& h3{margin:0;font-size:`,
24
+ `;line-height:1.2;}& svg{width:20px;height:20px;flex-shrink:0;}}& .chat-header-actions{display:flex;align-items:center;gap:12px;}`,
25
+ ``
26
+ ], ({ theme }) => theme.colors.light, ({ theme }) => theme.colors.dark, ({ theme }) => theme.colors.grayLight, ({ theme }) => theme.colors.dark, ({ theme }) => theme.fontSizes.h6.xs, ({ $variant, $side, $width, $isVisible, theme }) => {
27
+ if ($variant === "inline") return css([
28
+ `position:relative;width:100%;height:100%;border:solid 1px `,
29
+ `;border-radius:`,
30
+ `;overflow:hidden;`
31
+ ], theme.colors.grayLight, theme.spacing.radius.lg);
32
+ if ($variant === "fullscreen") return css([`position:fixed;inset:0;z-index:1000;transition:opacity 0.3s ease,visibility 0s linear 0s;opacity:1;visibility:visible;`, ` @media (prefers-reduced-motion:reduce){transition:none;}`], !$isVisible && css([`opacity:0;visibility:hidden;transition-delay:0s,0.3s;`]));
33
+ return css([
34
+ `position:fixed;top:0;bottom:0;`,
35
+ `:0;width:100%;height:100dvh;z-index:1000;transition:transform 0.3s ease,opacity 0.3s ease,visibility 0s linear 0s;transform:translateX(0);opacity:1;visibility:visible;`,
36
+ ` @media (prefers-reduced-motion:reduce){transition:none;}`,
37
+ `{width:`,
38
+ `px;border-`,
39
+ `:solid 1px `,
40
+ `;}`
41
+ ], $side, !$isVisible && css([`transform:translateX(`, `);opacity:0;visibility:hidden;transition-delay:0s,0s,0.3s;`], $side === "left" ? "-100%" : "100%"), mq("lg"), $width, $side === "left" ? "right" : "left", theme.colors.grayLight);
42
+ });
43
+ function LocalChatPanel({ children, $variant = "drawer", $side = "right", $width = DEFAULT_WIDTH, $title = "AI Assistant", $titleIcon = "Sparkles", $actions, $hideCloseButton, "aria-label": ariaLabel, ...props }, ref) {
44
+ const { isOpen, close, inputRef } = useContext(ChatContext);
45
+ const panelRef = useRef(null);
46
+ const isBelowLg = useBelowBreakpoint("lg");
47
+ const isOverlay = $variant !== "inline";
48
+ const isVisible = !isOverlay || isOpen;
49
+ const isModal = $variant === "fullscreen" && isOpen || $variant === "drawer" && isOpen && isBelowLg;
50
+ useLockBodyScroll(isModal);
51
+ useEffect(() => {
52
+ if (!isOverlay || !isOpen) return;
53
+ let frame = null;
54
+ let attempts = 0;
55
+ const tryFocus = () => {
56
+ frame = null;
57
+ const input = inputRef.current;
58
+ const panel = panelRef.current;
59
+ if (!input || !panel) return;
60
+ const active = document.activeElement;
61
+ if (active instanceof HTMLElement && panel.contains(active)) return;
62
+ input.focus();
63
+ if (document.activeElement !== input && attempts++ < 30) frame = requestAnimationFrame(tryFocus);
64
+ };
65
+ frame = requestAnimationFrame(tryFocus);
66
+ return () => {
67
+ if (frame !== null) cancelAnimationFrame(frame);
68
+ };
69
+ }, [
70
+ isOpen,
71
+ isOverlay,
72
+ inputRef
73
+ ]);
74
+ useEffect(() => {
75
+ if (!isModal || !panelRef.current) return;
76
+ const panel = panelRef.current;
77
+ const inertSiblings = [];
78
+ let current = panel;
79
+ while (current.parentElement) {
80
+ const parent = current.parentElement;
81
+ for (const sibling of Array.from(parent.children)) {
82
+ if (sibling === current || !(sibling instanceof HTMLElement)) continue;
83
+ inertSiblings.push({
84
+ element: sibling,
85
+ wasInert: sibling.inert
86
+ });
87
+ sibling.inert = true;
88
+ }
89
+ if (parent === document.body) break;
90
+ current = parent;
91
+ }
92
+ const containFocus = (event) => {
93
+ if (event.target instanceof Node && !panel.contains(event.target)) inputRef.current?.focus();
94
+ };
95
+ document.addEventListener("focusin", containFocus);
96
+ return () => {
97
+ document.removeEventListener("focusin", containFocus);
98
+ for (const { element, wasInert } of inertSiblings) element.inert = wasInert;
99
+ };
100
+ }, [isModal, inputRef]);
101
+ const handleKeyDown = (event) => {
102
+ if (!isModal) return;
103
+ if (event.key === "Escape") {
104
+ event.preventDefault();
105
+ event.stopPropagation();
106
+ close();
107
+ return;
108
+ }
109
+ if (event.key !== "Tab") return;
110
+ const focusable = Array.from(panelRef.current?.querySelectorAll(FOCUSABLE_SELECTOR) ?? []).filter((element) => element.offsetParent !== null && element.getAttribute("aria-hidden") !== "true");
111
+ if (focusable.length === 0) {
112
+ event.preventDefault();
113
+ return;
114
+ }
115
+ const first = focusable[0];
116
+ const last = focusable[focusable.length - 1];
117
+ if (event.shiftKey && document.activeElement === first) {
118
+ event.preventDefault();
119
+ last.focus();
120
+ } else if (!event.shiftKey && document.activeElement === last) {
121
+ event.preventDefault();
122
+ first.focus();
123
+ }
124
+ };
125
+ const showHeader = $title !== null || $actions || isOverlay && !$hideCloseButton;
126
+ return /*#__PURE__*/ jsxs(StyledChatPanel, {
127
+ $variant,
128
+ $side,
129
+ $width,
130
+ $isVisible: isVisible,
131
+ inert: isOverlay ? !isOpen : void 0,
132
+ "aria-hidden": isOverlay ? !isOpen : void 0,
133
+ role: isModal ? "dialog" : isOverlay ? "complementary" : void 0,
134
+ "aria-modal": isModal ? "true" : void 0,
135
+ "aria-label": ariaLabel ?? (typeof $title === "string" ? $title : void 0),
136
+ "data-chat-panel": true,
137
+ onKeyDownCapture: handleKeyDown,
138
+ ...props,
139
+ ref: (node) => {
140
+ panelRef.current = node;
141
+ if (typeof ref === "function") ref(node);
142
+ else if (ref) ref.current = node;
143
+ },
144
+ children: [showHeader && /*#__PURE__*/ jsxs("div", {
145
+ className: "chat-header",
146
+ children: [/*#__PURE__*/ jsxs("span", {
147
+ className: "chat-header-title",
148
+ children: [$titleIcon && /*#__PURE__*/ jsx(Icon, { name: $titleIcon }), $title !== null && /*#__PURE__*/ jsx("h3", { children: $title })]
149
+ }), /*#__PURE__*/ jsxs("span", {
150
+ className: "chat-header-actions",
151
+ children: [$actions, isOverlay && !$hideCloseButton && /*#__PURE__*/ jsx(IconButton, {
152
+ onClick: () => close(),
153
+ "aria-label": "Close chat",
154
+ title: "Close chat",
155
+ children: /*#__PURE__*/ jsx(Icon, { name: "X" })
156
+ })]
157
+ })]
158
+ }), children]
159
+ });
160
+ }
161
+ var ChatPanel = /*#__PURE__*/ forwardRef(LocalChatPanel);
162
+ //#endregion
163
+ export { ChatPanel };
@@ -0,0 +1,86 @@
1
+ import { default as React } from 'react';
2
+ export type ChatRole = "user" | "assistant";
3
+ export interface ChatSourceData {
4
+ id: string;
5
+ label: string;
6
+ href: string;
7
+ }
8
+ export interface ChatMessageData {
9
+ id: string;
10
+ role: ChatRole;
11
+ /** Rendered as-is: plain text while streaming, rich markup when done. */
12
+ content: React.ReactNode;
13
+ /** Plain-text mirror of `content`, used to build the history for onSend. */
14
+ text?: string;
15
+ sources?: ChatSourceData[];
16
+ }
17
+ export interface ChatHistoryEntry {
18
+ role: ChatRole;
19
+ content: string;
20
+ }
21
+ export interface ChatSendContext {
22
+ /** Aborted by reset() and on unmount; pass it to fetch. */
23
+ signal: AbortSignal;
24
+ /** The conversation before this question, oldest first. */
25
+ history: ChatHistoryEntry[];
26
+ /**
27
+ * Creates the assistant reply on the first call and patches the same
28
+ * message on every later call, so streaming updates one bubble and the
29
+ * typing indicator stays put until real content arrives.
30
+ */
31
+ setAssistant: (content: React.ReactNode, options?: {
32
+ text?: string;
33
+ sources?: ChatSourceData[];
34
+ }) => void;
35
+ }
36
+ /**
37
+ * The transport, supplied by the app: take the question, talk to a server
38
+ * however you like, and stream the reply through `setAssistant`. A thrown
39
+ * error lands in `error`; an abort is swallowed.
40
+ */
41
+ export type ChatSendHandler = (question: string, context: ChatSendContext) => void | Promise<void>;
42
+ interface ChatContextValue {
43
+ isOpen: boolean;
44
+ open: (returnFocusTo?: HTMLElement | null) => void;
45
+ close: (restoreFocus?: boolean) => HTMLElement | null;
46
+ toggle: () => void;
47
+ messages: ChatMessageData[];
48
+ input: string;
49
+ setInput: (value: string) => void;
50
+ loading: boolean;
51
+ error: string | null;
52
+ /** Sends `text`, or the current input when omitted. No-op while loading. */
53
+ send: (text?: string) => void;
54
+ /**
55
+ * Opens the panel and hands `question` to the assistant. While a reply is
56
+ * still streaming the question is pre-filled instead, ready to send.
57
+ */
58
+ ask: (question: string, returnFocusTo?: HTMLElement | null) => void;
59
+ /** Aborts any in-flight reply and restores the initial greeting. */
60
+ reset: () => void;
61
+ inputRef: React.RefObject<HTMLTextAreaElement | null>;
62
+ }
63
+ declare const ChatContext: React.Context<ChatContextValue>;
64
+ declare function useChat(): ChatContextValue;
65
+ export interface ChatProviderProps {
66
+ children: React.ReactNode;
67
+ /**
68
+ * The transport. Optional when $showcase is on, where the commands can
69
+ * carry a demo by themselves; required for real answers.
70
+ */
71
+ onSend?: ChatSendHandler;
72
+ /** Assistant message seeded on first open and after reset. Pass null for none. */
73
+ greeting?: React.ReactNode;
74
+ /** How many prior messages the history handed to onSend may contain. */
75
+ historyLimit?: number;
76
+ /** Cmd/Ctrl + this key toggles the panel. Pass null to disable. */
77
+ shortcut?: string | null;
78
+ /**
79
+ * Demo mode: chat input matching a showcase command ("help", "list",
80
+ * "callout", …) is answered locally with a live rendering of that element
81
+ * instead of going to onSend. Anything else still goes to the transport.
82
+ */
83
+ $showcase?: boolean;
84
+ }
85
+ declare function ChatProvider({ children, onSend, greeting, historyLimit, shortcut, $showcase, }: ChatProviderProps): React.JSX.Element;
86
+ export { ChatContext, ChatProvider, useChat };
@@ -0,0 +1,295 @@
1
+ "use client";
2
+ "use client";
3
+ import { resolveShowcaseCommand } from "./chat-showcase.js";
4
+ import { jsx } from "react/jsx-runtime";
5
+ import { createContext, useCallback, useContext, useEffect, useRef, useState } from "react";
6
+ //#region src/lib/chat-provider.tsx
7
+ var ChatContext = /*#__PURE__*/ createContext({
8
+ isOpen: false,
9
+ open: () => {},
10
+ close: () => null,
11
+ toggle: () => {},
12
+ messages: [],
13
+ input: "",
14
+ setInput: () => {},
15
+ loading: false,
16
+ error: null,
17
+ send: () => {},
18
+ ask: () => {},
19
+ reset: () => {},
20
+ inputRef: { current: null }
21
+ });
22
+ function useChat() {
23
+ return useContext(ChatContext);
24
+ }
25
+ var DEFAULT_GREETING = "Hey there, how can I assist you?";
26
+ var SHOWCASE_GREETING = "Hey there! Type \"help\" to see everything I can render.";
27
+ var GREETING_ID = "chat-greeting";
28
+ var HISTORY_ENTRY_MAX_CHARS = 4e3;
29
+ function ChatProvider({ children, onSend, greeting, historyLimit = 20, shortcut = "i", $showcase = false }) {
30
+ const resolvedGreeting = greeting !== void 0 ? greeting : $showcase ? SHOWCASE_GREETING : DEFAULT_GREETING;
31
+ const [isOpen, setIsOpen] = useState(false);
32
+ const [input, setInput] = useState("");
33
+ const [loading, setLoading] = useState(false);
34
+ const [error, setError] = useState(null);
35
+ const [messages, setMessages] = useState(() => resolvedGreeting === null ? [] : [{
36
+ id: GREETING_ID,
37
+ role: "assistant",
38
+ content: resolvedGreeting,
39
+ text: typeof resolvedGreeting === "string" ? resolvedGreeting : void 0
40
+ }]);
41
+ const inputRef = useRef(null);
42
+ const abortRef = useRef(null);
43
+ const idCounter = useRef(0);
44
+ const isOpenRef = useRef(isOpen);
45
+ const loadingRef = useRef(loading);
46
+ const messagesRef = useRef(messages);
47
+ const onSendRef = useRef(onSend);
48
+ const greetingRef = useRef(resolvedGreeting);
49
+ const inputValueRef = useRef(input);
50
+ useEffect(() => {
51
+ isOpenRef.current = isOpen;
52
+ loadingRef.current = loading;
53
+ messagesRef.current = messages;
54
+ inputValueRef.current = input;
55
+ onSendRef.current = onSend;
56
+ greetingRef.current = resolvedGreeting;
57
+ });
58
+ const openerRef = useRef(null);
59
+ const pendingRestoreFocusRef = useRef(null);
60
+ const focusTimeoutRef = useRef(null);
61
+ const restoreFocusTimeoutRef = useRef(null);
62
+ const nextId = useCallback(() => `chat-msg-${idCounter.current++}`, []);
63
+ const greetingMessages = useCallback(() => {
64
+ const currentGreeting = greetingRef.current;
65
+ if (currentGreeting === null || currentGreeting === void 0) return [];
66
+ return [{
67
+ id: GREETING_ID,
68
+ role: "assistant",
69
+ content: currentGreeting,
70
+ text: typeof currentGreeting === "string" ? currentGreeting : void 0
71
+ }];
72
+ }, []);
73
+ const showPanel = useCallback((returnFocusTo) => {
74
+ const pendingOpener = pendingRestoreFocusRef.current;
75
+ if (restoreFocusTimeoutRef.current) {
76
+ clearTimeout(restoreFocusTimeoutRef.current);
77
+ restoreFocusTimeoutRef.current = null;
78
+ }
79
+ pendingRestoreFocusRef.current = null;
80
+ if (!isOpenRef.current) {
81
+ const activeElement = document.activeElement;
82
+ openerRef.current = returnFocusTo?.isConnected === true ? returnFocusTo : pendingOpener?.isConnected === true ? pendingOpener : activeElement instanceof HTMLElement && activeElement !== document.body ? activeElement : null;
83
+ }
84
+ isOpenRef.current = true;
85
+ setIsOpen(true);
86
+ if (focusTimeoutRef.current) clearTimeout(focusTimeoutRef.current);
87
+ focusTimeoutRef.current = setTimeout(() => {
88
+ focusTimeoutRef.current = null;
89
+ inputRef.current?.focus();
90
+ }, 0);
91
+ }, []);
92
+ const open = useCallback((returnFocusTo) => {
93
+ showPanel(returnFocusTo);
94
+ setMessages((prev) => prev.length === 0 ? greetingMessages() : prev);
95
+ }, [showPanel, greetingMessages]);
96
+ const close = useCallback((restoreFocus = true) => {
97
+ const pendingOpener = pendingRestoreFocusRef.current;
98
+ if (!restoreFocus && restoreFocusTimeoutRef.current) {
99
+ clearTimeout(restoreFocusTimeoutRef.current);
100
+ restoreFocusTimeoutRef.current = null;
101
+ pendingRestoreFocusRef.current = null;
102
+ }
103
+ if (!isOpenRef.current) return pendingOpener;
104
+ if (focusTimeoutRef.current) {
105
+ clearTimeout(focusTimeoutRef.current);
106
+ focusTimeoutRef.current = null;
107
+ }
108
+ const activeElement = document.activeElement;
109
+ if (activeElement instanceof HTMLElement && activeElement.closest("[data-chat-panel]")) activeElement.blur();
110
+ const opener = openerRef.current;
111
+ openerRef.current = null;
112
+ isOpenRef.current = false;
113
+ setIsOpen(false);
114
+ if (restoreFocusTimeoutRef.current) clearTimeout(restoreFocusTimeoutRef.current);
115
+ if (restoreFocus) {
116
+ pendingRestoreFocusRef.current = opener;
117
+ restoreFocusTimeoutRef.current = setTimeout(() => {
118
+ const focusTarget = pendingRestoreFocusRef.current;
119
+ restoreFocusTimeoutRef.current = null;
120
+ pendingRestoreFocusRef.current = null;
121
+ if (focusTarget?.isConnected) focusTarget.focus();
122
+ }, 0);
123
+ } else pendingRestoreFocusRef.current = null;
124
+ return opener;
125
+ }, []);
126
+ const toggle = useCallback(() => {
127
+ if (isOpenRef.current) close();
128
+ else open();
129
+ }, [close, open]);
130
+ const runSend = useCallback(async (question, returnFocusTo) => {
131
+ setInput("");
132
+ showPanel(returnFocusTo);
133
+ const base = messagesRef.current.length > 0 ? messagesRef.current : greetingMessages();
134
+ if ($showcase) {
135
+ const demo = resolveShowcaseCommand(question);
136
+ if (demo) {
137
+ const withDemo = [
138
+ ...base,
139
+ {
140
+ id: nextId(),
141
+ role: "user",
142
+ content: question,
143
+ text: question
144
+ },
145
+ {
146
+ id: nextId(),
147
+ role: "assistant",
148
+ content: demo
149
+ }
150
+ ];
151
+ messagesRef.current = withDemo;
152
+ setMessages(withDemo);
153
+ setError(null);
154
+ return;
155
+ }
156
+ }
157
+ const handler = onSendRef.current;
158
+ if (!handler) {
159
+ const hint = $showcase ? "I can only render showcase demos here. Type \"help\" to see the available commands." : "No transport is wired up. Pass onSend to ChatProvider to get answers.";
160
+ const withHint = [
161
+ ...base,
162
+ {
163
+ id: nextId(),
164
+ role: "user",
165
+ content: question,
166
+ text: question
167
+ },
168
+ {
169
+ id: nextId(),
170
+ role: "assistant",
171
+ content: hint,
172
+ text: hint
173
+ }
174
+ ];
175
+ messagesRef.current = withHint;
176
+ setMessages(withHint);
177
+ setError(null);
178
+ return;
179
+ }
180
+ setLoading(true);
181
+ loadingRef.current = true;
182
+ setError(null);
183
+ const history = base.filter((message) => (message.text ?? "").trim() !== "").slice(-historyLimit).map((message) => ({
184
+ role: message.role,
185
+ content: message.text.slice(0, HISTORY_ENTRY_MAX_CHARS)
186
+ }));
187
+ const withQuestion = [...base, {
188
+ id: nextId(),
189
+ role: "user",
190
+ content: question,
191
+ text: question
192
+ }];
193
+ messagesRef.current = withQuestion;
194
+ setMessages(withQuestion);
195
+ const controller = new AbortController();
196
+ abortRef.current = controller;
197
+ let assistantId = null;
198
+ const setAssistant = (content, options) => {
199
+ if (assistantId === null) assistantId = nextId();
200
+ const patch = {
201
+ id: assistantId,
202
+ role: "assistant",
203
+ content,
204
+ text: options?.text ?? (typeof content === "string" ? content : void 0),
205
+ sources: options?.sources
206
+ };
207
+ setMessages((prev) => {
208
+ const index = prev.findIndex((message) => message.id === assistantId);
209
+ if (index === -1) return [...prev, patch];
210
+ const next = [...prev];
211
+ next[index] = patch;
212
+ return next;
213
+ });
214
+ };
215
+ try {
216
+ await handler(question, {
217
+ signal: controller.signal,
218
+ history,
219
+ setAssistant
220
+ });
221
+ } catch (err) {
222
+ if (err instanceof DOMException && err.name === "AbortError") return;
223
+ setError(err instanceof Error ? err.message : "Unknown error");
224
+ } finally {
225
+ if (abortRef.current === controller) abortRef.current = null;
226
+ setLoading(false);
227
+ loadingRef.current = false;
228
+ }
229
+ }, [
230
+ showPanel,
231
+ greetingMessages,
232
+ historyLimit,
233
+ nextId,
234
+ $showcase
235
+ ]);
236
+ const send = useCallback((text) => {
237
+ const question = (text ?? inputValueRef.current).trim();
238
+ if (question === "" || loadingRef.current) return;
239
+ runSend(question);
240
+ }, [runSend]);
241
+ const ask = useCallback((question, returnFocusTo) => {
242
+ const trimmed = question.trim();
243
+ if (trimmed === "") return;
244
+ if (loadingRef.current) {
245
+ showPanel(returnFocusTo);
246
+ setInput(trimmed);
247
+ } else runSend(trimmed, returnFocusTo);
248
+ }, [showPanel, runSend]);
249
+ const reset = useCallback(() => {
250
+ abortRef.current?.abort();
251
+ abortRef.current = null;
252
+ setLoading(false);
253
+ loadingRef.current = false;
254
+ setError(null);
255
+ setMessages(greetingMessages());
256
+ }, [greetingMessages]);
257
+ useEffect(() => {
258
+ if (!shortcut) return;
259
+ function handleKeyDown(event) {
260
+ if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === shortcut) {
261
+ event.preventDefault();
262
+ toggle();
263
+ }
264
+ }
265
+ document.addEventListener("keydown", handleKeyDown);
266
+ return () => document.removeEventListener("keydown", handleKeyDown);
267
+ }, [shortcut, toggle]);
268
+ useEffect(() => {
269
+ return () => {
270
+ if (focusTimeoutRef.current) clearTimeout(focusTimeoutRef.current);
271
+ if (restoreFocusTimeoutRef.current) clearTimeout(restoreFocusTimeoutRef.current);
272
+ abortRef.current?.abort();
273
+ };
274
+ }, []);
275
+ return /*#__PURE__*/ jsx(ChatContext.Provider, {
276
+ value: {
277
+ isOpen,
278
+ open,
279
+ close,
280
+ toggle,
281
+ messages,
282
+ input,
283
+ setInput,
284
+ loading,
285
+ error,
286
+ send,
287
+ ask,
288
+ reset,
289
+ inputRef
290
+ },
291
+ children
292
+ });
293
+ }
294
+ //#endregion
295
+ export { ChatContext, ChatProvider, useChat };
@@ -0,0 +1,18 @@
1
+ import { default as React } from 'react';
2
+ /**
3
+ * The showcase command set behind ChatProvider's $showcase prop: type a
4
+ * command into the chat and the reply is a live demo of that element,
5
+ * rendered locally without touching the transport. `help` lists them all.
6
+ */
7
+ export interface ShowcaseCommand {
8
+ name: string;
9
+ description: string;
10
+ render: () => React.ReactNode;
11
+ }
12
+ export declare const showcaseCommands: ShowcaseCommand[];
13
+ /**
14
+ * Matches a chat question against the showcase commands ("help", "/help" and
15
+ * any casing all work) and returns the rendered demo, or null when the input
16
+ * is not a command and should go to the transport.
17
+ */
18
+ export declare function resolveShowcaseCommand(input: string): React.ReactNode | null;