@jini-ai/chat 0.5.7 → 0.5.8
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/dist/core/ag-ui/entry.js +2 -0
- package/dist/core/assistant-client/durable-run-subscription.d.ts +4 -1
- package/dist/core/assistant-client/durable-run-subscription.js +13 -4
- package/dist/core/events.d.ts +3 -0
- package/dist/core/history-tool-metadata.d.ts +23 -0
- package/dist/core/history-tool-metadata.js +13 -0
- package/dist/core/index.d.ts +3 -0
- package/dist/core/index.js +3 -0
- package/dist/core/persistence/title.d.ts +24 -0
- package/dist/core/persistence/title.js +60 -9
- package/dist/core/run-activity.d.ts +15 -2
- package/dist/core/run-activity.js +6 -4
- package/dist/core/run-events/entry.js +2 -0
- package/dist/core/tool-call-detail.d.ts +15 -0
- package/dist/core/tool-call-detail.js +18 -0
- package/dist/core/turn-usage.d.ts +42 -3
- package/dist/core/turn-usage.js +101 -4
- package/dist/core/usage-format.d.ts +10 -0
- package/dist/core/usage-format.js +15 -0
- package/dist/embed-widget/browser/lazy-loader.d.ts +34 -0
- package/dist/embed-widget/browser/lazy-loader.js +116 -0
- package/dist/embed-widget/react/dialog.hooks.d.ts +28 -0
- package/dist/embed-widget/react/dialog.hooks.js +54 -0
- package/dist/embed-widget/react/embed.d.ts +1 -0
- package/dist/embed-widget/react/embed.js +5 -3
- package/dist/embed-widget/react/header.d.ts +3 -1
- package/dist/embed-widget/react/header.hooks.d.ts +13 -0
- package/dist/embed-widget/react/header.hooks.js +32 -0
- package/dist/embed-widget/react/header.js +4 -14
- package/dist/embed-widget/vanilla/index.d.ts +63 -0
- package/dist/embed-widget/vanilla/index.js +319 -0
- package/dist/embed-widget/vanilla/state.d.ts +48 -0
- package/dist/embed-widget/vanilla/state.js +120 -0
- package/dist/embed-widget/vanilla/style.css +29 -0
- package/dist/react/components/A2uiSurfaceCard.d.ts +5 -1
- package/dist/react/components/A2uiSurfaceCard.js +9 -9
- package/dist/react/components/AttachmentPreviewModal.hooks.d.ts +7 -0
- package/dist/react/components/AttachmentPreviewModal.hooks.js +1 -1
- package/dist/react/components/ChatFab.d.ts +4 -1
- package/dist/react/components/ChatFab.js +2 -2
- package/dist/react/components/ConversationList.d.ts +8 -1
- package/dist/react/components/ConversationList.hooks.d.ts +18 -0
- package/dist/react/components/ConversationList.hooks.js +25 -0
- package/dist/react/components/ConversationList.js +12 -5
- package/dist/react/components/ConversationList.rules.d.ts +25 -0
- package/dist/react/components/ConversationList.rules.js +19 -0
- package/dist/react/components/McpUiSurfaceCard.d.ts +1 -1
- package/dist/react/components/McpUiSurfaceCard.js +8 -4
- package/dist/react/components/MessageList.d.ts +1 -1
- package/dist/react/components/MessageList.hooks.d.ts +4 -0
- package/dist/react/components/MessageList.hooks.js +6 -1
- package/dist/react/components/MessageList.js +4 -3
- package/dist/react/components/MessageRow.d.ts +17 -0
- package/dist/react/components/MessageRow.js +54 -35
- package/dist/react/components/MessageRow.usage.d.ts +48 -0
- package/dist/react/components/MessageRow.usage.js +47 -0
- package/dist/react/components/MidRunUserMessage.d.ts +2 -12
- package/dist/react/components/MidRunUserMessage.js +4 -2
- package/dist/react/components/SentAttachment.d.ts +19 -0
- package/dist/react/components/SentAttachment.hooks.d.ts +44 -0
- package/dist/react/components/SentAttachment.hooks.js +56 -0
- package/dist/react/components/SentAttachment.js +14 -0
- package/dist/react/components/ToolCallGroup.d.ts +12 -0
- package/dist/react/components/ToolCallGroup.hooks.d.ts +20 -0
- package/dist/react/components/ToolCallGroup.hooks.js +23 -0
- package/dist/react/components/ToolCallGroup.js +13 -0
- package/dist/react/components/ToolCard.js +25 -17
- package/dist/react/components/mcp-ui-surface-state.d.ts +21 -0
- package/dist/react/components/mcp-ui-surface-state.js +26 -0
- package/dist/react/ext-event-renderer-registry.d.ts +7 -0
- package/dist/react/features/chat-pane/components/ChatPane.d.ts +1 -1
- package/dist/react/features/chat-pane/components/ChatPane.js +11 -6
- package/dist/react/features/chat-pane/hooks/useChatPane.hooks.d.ts +1 -0
- package/dist/react/features/chat-pane/hooks/useChatPane.hooks.js +16 -10
- package/dist/react/features/chat-pane/hooks/useChatPaneRuntimeInventory.hooks.js +4 -0
- package/dist/react/features/chat-pane/hooks/useChatPaneSuggestion.hooks.d.ts +9 -0
- package/dist/react/features/chat-pane/hooks/useChatPaneSuggestion.hooks.js +11 -0
- package/dist/react/features/chat-pane/hooks/useChatPaneSurface.hooks.d.ts +17 -0
- package/dist/react/features/chat-pane/hooks/useChatPaneSurface.hooks.js +15 -0
- package/dist/react/features/chat-pane/rules.d.ts +14 -0
- package/dist/react/features/chat-pane/rules.js +18 -0
- package/dist/react/features/chat-pane/styles.d.ts +1 -1
- package/dist/react/features/chat-pane/styles.js +85 -1
- package/dist/react/features/chat-pane/types.d.ts +16 -1
- package/dist/react/hooks/useAnsweredCards.d.ts +20 -0
- package/dist/react/hooks/useAnsweredCards.js +23 -0
- package/dist/react/hooks/useConversation.d.ts +8 -0
- package/dist/react/hooks/useConversation.js +34 -0
- package/dist/react/hooks/useRunActivity.d.ts +5 -3
- package/dist/react/hooks/useRunActivity.js +4 -3
- package/dist/react/index.d.ts +4 -0
- package/dist/react/index.js +3 -0
- package/dist/react/message-blocks.d.ts +22 -3
- package/dist/react/message-blocks.js +25 -3
- package/dist/react/tool-call-display.d.ts +90 -0
- package/dist/react/tool-call-display.js +97 -0
- package/dist/store/ports.d.ts +25 -2
- package/dist/store/runtime/entry.d.ts +1 -0
- package/dist/store/runtime/entry.js +1 -0
- package/dist/store/runtime/history-tools.d.ts +27 -0
- package/dist/store/runtime/history-tools.js +169 -0
- package/dist/store/sql/store.js +52 -0
- package/dist/testing/index.d.ts +7 -0
- package/dist/testing/index.js +22 -0
- package/package.json +48 -18
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/** Shared bubble styles for server placeholders and mounted DOM widgets; no runtime dependencies. */
|
|
2
|
+
export declare const CHAT_BUBBLE_STYLE = ".jini-chat-bubble{position:fixed;right:var(--jini-embed-right,24px);bottom:24px;width:56px;height:56px;border:0;border-radius:50%;background:var(--jini-chat-accent,var(--color-primary,#2563eb));color:var(--jini-chat-accent-text,#fff);box-shadow:0 4px 16px #0003;cursor:pointer;z-index:var(--jini-embed-z-index,1000);display:grid;place-items:center}\n.jini-chat-bubble[hidden]{display:none!important}\n.jini-chat-bubble svg{width:24px;height:24px}\n.jini-chat-bubble:focus-visible{outline:3px solid var(--jini-chat-accent,#2563eb);outline-offset:3px}\n@media(max-width:480px){.jini-chat-bubble{right:16px;bottom:16px}}\n.jini-chat-bubble[aria-busy=\"true\"]{cursor:progress;opacity:.65}\n.jini-chat-bubble[data-load-failed=\"true\"]{outline:2px solid var(--jini-chat-error,#b91c1c);outline-offset:3px}";
|
|
3
|
+
export interface LazyChatLoaderArgs {
|
|
4
|
+
readonly button: HTMLButtonElement;
|
|
5
|
+
readonly load: (args: Record<string, never>) => Promise<void>;
|
|
6
|
+
readonly open: (args: {
|
|
7
|
+
focus?: boolean;
|
|
8
|
+
}) => void;
|
|
9
|
+
readonly resume: boolean;
|
|
10
|
+
readonly deepLink: boolean;
|
|
11
|
+
readonly preload?: boolean;
|
|
12
|
+
readonly loadingLabel: string;
|
|
13
|
+
readonly retryLabel: string;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Activates a host-rendered button without importing chat, React, icons or storage.
|
|
17
|
+
* One in-flight load serves repeated activation; failures remain retryable. Native button
|
|
18
|
+
* keyboard activation also emits click; preventDefault avoids a second toggle after keydown.
|
|
19
|
+
*/
|
|
20
|
+
export declare function installLazyChatLoader(args: LazyChatLoaderArgs, _options?: {}): {
|
|
21
|
+
activate: ({ open, focus }?: {
|
|
22
|
+
open?: boolean;
|
|
23
|
+
focus?: boolean;
|
|
24
|
+
}, _options?: {}) => Promise<void>;
|
|
25
|
+
dispose(_input?: Record<string, never>, _options?: {}): void;
|
|
26
|
+
};
|
|
27
|
+
/** Loads CSS before a self-mounting classic script; failed elements are removed for retry. */
|
|
28
|
+
export declare function loadChatAssets({ document, scriptUrl, styleUrl, mount }: {
|
|
29
|
+
document: Document;
|
|
30
|
+
scriptUrl: string;
|
|
31
|
+
styleUrl: string;
|
|
32
|
+
mount?: HTMLElement;
|
|
33
|
+
}, _options?: {}): Promise<void>;
|
|
34
|
+
//# sourceMappingURL=lazy-loader.d.ts.map
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/** Shared bubble styles for server placeholders and mounted DOM widgets; no runtime dependencies. */
|
|
2
|
+
export const CHAT_BUBBLE_STYLE = '.jini-chat-bubble{position:fixed;right:var(--jini-embed-right,24px);bottom:24px;width:56px;height:56px;border:0;border-radius:50%;background:var(--jini-chat-accent,var(--color-primary,#2563eb));color:var(--jini-chat-accent-text,#fff);box-shadow:0 4px 16px #0003;cursor:pointer;z-index:var(--jini-embed-z-index,1000);display:grid;place-items:center}\n.jini-chat-bubble[hidden]{display:none!important}\n.jini-chat-bubble svg{width:24px;height:24px}\n.jini-chat-bubble:focus-visible{outline:3px solid var(--jini-chat-accent,#2563eb);outline-offset:3px}\n@media(max-width:480px){.jini-chat-bubble{right:16px;bottom:16px}}\n.jini-chat-bubble[aria-busy="true"]{cursor:progress;opacity:.65}\n.jini-chat-bubble[data-load-failed="true"]{outline:2px solid var(--jini-chat-error,#b91c1c);outline-offset:3px}';
|
|
3
|
+
/**
|
|
4
|
+
* Activates a host-rendered button without importing chat, React, icons or storage.
|
|
5
|
+
* One in-flight load serves repeated activation; failures remain retryable. Native button
|
|
6
|
+
* keyboard activation also emits click; preventDefault avoids a second toggle after keydown.
|
|
7
|
+
*/
|
|
8
|
+
export function installLazyChatLoader(args, _options = {}) {
|
|
9
|
+
let pending = null;
|
|
10
|
+
let ready = false;
|
|
11
|
+
let disposed = false;
|
|
12
|
+
let shouldOpen = false;
|
|
13
|
+
let shouldFocus = false;
|
|
14
|
+
const initialContent = args.button.innerHTML;
|
|
15
|
+
const label = args.button.getAttribute('aria-label') ?? '';
|
|
16
|
+
const activate = async ({ open = true, focus = open } = {}, _options = {}) => {
|
|
17
|
+
if (disposed)
|
|
18
|
+
return;
|
|
19
|
+
if (!pending) {
|
|
20
|
+
shouldOpen = false;
|
|
21
|
+
shouldFocus = false;
|
|
22
|
+
}
|
|
23
|
+
shouldOpen ||= open;
|
|
24
|
+
shouldFocus ||= focus;
|
|
25
|
+
if (pending)
|
|
26
|
+
return pending;
|
|
27
|
+
args.button.setAttribute('aria-busy', 'true');
|
|
28
|
+
args.button.setAttribute('aria-label', args.loadingLabel);
|
|
29
|
+
args.button.textContent = args.loadingLabel;
|
|
30
|
+
delete args.button.dataset.loadFailed;
|
|
31
|
+
// Defer through a promise so a synchronous host load failure follows the same retry path.
|
|
32
|
+
pending = Promise.resolve().then(() => { if (!ready)
|
|
33
|
+
return args.load({}); }).then(() => {
|
|
34
|
+
if (disposed)
|
|
35
|
+
return;
|
|
36
|
+
// Asset readiness survives an open failure; retry must never evaluate the script twice.
|
|
37
|
+
ready = true;
|
|
38
|
+
if (shouldOpen)
|
|
39
|
+
args.open({ focus: shouldFocus });
|
|
40
|
+
args.button.setAttribute('aria-label', label);
|
|
41
|
+
args.button.innerHTML = initialContent;
|
|
42
|
+
args.button.hidden = true;
|
|
43
|
+
}).catch(() => {
|
|
44
|
+
if (!disposed) {
|
|
45
|
+
args.button.hidden = false;
|
|
46
|
+
args.button.dataset.loadFailed = 'true';
|
|
47
|
+
args.button.setAttribute('aria-label', args.retryLabel);
|
|
48
|
+
args.button.textContent = args.retryLabel;
|
|
49
|
+
}
|
|
50
|
+
}).finally(() => {
|
|
51
|
+
pending = null;
|
|
52
|
+
args.button.removeAttribute('aria-busy');
|
|
53
|
+
});
|
|
54
|
+
return pending;
|
|
55
|
+
};
|
|
56
|
+
const click = () => { void activate({}); };
|
|
57
|
+
const keydown = (event) => {
|
|
58
|
+
if (event.key !== 'Enter' && event.key !== ' ')
|
|
59
|
+
return;
|
|
60
|
+
event.preventDefault();
|
|
61
|
+
void activate({});
|
|
62
|
+
};
|
|
63
|
+
args.button.addEventListener('click', click);
|
|
64
|
+
args.button.addEventListener('keydown', keydown);
|
|
65
|
+
args.button.dataset.loaderReady = 'true';
|
|
66
|
+
if (args.resume || args.deepLink)
|
|
67
|
+
void activate({ focus: args.deepLink });
|
|
68
|
+
else if (args.preload)
|
|
69
|
+
void activate({ open: false });
|
|
70
|
+
return {
|
|
71
|
+
activate,
|
|
72
|
+
dispose(_input = {}, _options = {}) {
|
|
73
|
+
disposed = true;
|
|
74
|
+
args.button.removeEventListener('click', click);
|
|
75
|
+
args.button.removeEventListener('keydown', keydown);
|
|
76
|
+
args.button.setAttribute('aria-label', label);
|
|
77
|
+
args.button.innerHTML = initialContent;
|
|
78
|
+
args.button.removeAttribute('aria-busy');
|
|
79
|
+
delete args.button.dataset.loadFailed;
|
|
80
|
+
delete args.button.dataset.loaderReady;
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
/** Loads CSS before a self-mounting classic script; failed elements are removed for retry. */
|
|
85
|
+
export async function loadChatAssets({ document, scriptUrl, styleUrl, mount }, _options = {}) {
|
|
86
|
+
const stylesheet = document.createElement('link');
|
|
87
|
+
stylesheet.rel = 'stylesheet';
|
|
88
|
+
stylesheet.href = styleUrl;
|
|
89
|
+
const script = document.createElement('script');
|
|
90
|
+
script.src = scriptUrl;
|
|
91
|
+
const loaded = (element) => new Promise((resolve, reject) => {
|
|
92
|
+
element.onload = () => resolve();
|
|
93
|
+
element.onerror = () => reject(new Error('Chat assets could not be loaded'));
|
|
94
|
+
document.head.appendChild(element);
|
|
95
|
+
});
|
|
96
|
+
try {
|
|
97
|
+
await loaded(stylesheet);
|
|
98
|
+
await loaded(script);
|
|
99
|
+
// A classic script's load event proves evaluation, not React commit or a successful mount.
|
|
100
|
+
// Optional for legacy hosts; lazy visitor hosts supply their root as the readiness boundary.
|
|
101
|
+
if (mount && mount.dataset.chatMounted !== 'true') {
|
|
102
|
+
await new Promise((resolve, reject) => {
|
|
103
|
+
const ready = () => { cleanup(); resolve(); };
|
|
104
|
+
const timeout = setTimeout(() => { cleanup(); reject(new Error('Chat widget did not mount')); }, 10_000);
|
|
105
|
+
const cleanup = () => { clearTimeout(timeout); mount.removeEventListener('jini-chat-mounted', ready); };
|
|
106
|
+
mount.addEventListener('jini-chat-mounted', ready);
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
catch (error) {
|
|
111
|
+
stylesheet.remove();
|
|
112
|
+
script.remove();
|
|
113
|
+
throw error;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
//# sourceMappingURL=lazy-loader.js.map
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { type RefObject } from 'react';
|
|
2
|
+
/**
|
|
3
|
+
* Shares the panel-kit trap across embeds. Escape respects a child popup's preventDefault;
|
|
4
|
+
* Closing restores the mounted launcher, including after a lazy-loader handoff.
|
|
5
|
+
* A changed resetKey focuses the replacement composer after the host remounts the pane.
|
|
6
|
+
* @complexity O(focusable controls) per Tab; O(1) listeners and retained state.
|
|
7
|
+
*/
|
|
8
|
+
export declare function useEmbedChatDialog({ open, onClose, panelRef, launcherRef }: {
|
|
9
|
+
open: boolean;
|
|
10
|
+
onClose: () => void;
|
|
11
|
+
panelRef: RefObject<HTMLElement | null>;
|
|
12
|
+
launcherRef: RefObject<HTMLElement | null>;
|
|
13
|
+
}, { resetKey }?: {
|
|
14
|
+
resetKey?: number | undefined;
|
|
15
|
+
}): void;
|
|
16
|
+
/** Reusable state/ref wiring for the packaged embed. O(1) besides the shared trap's Tab scan. */
|
|
17
|
+
export declare function useEmbedChatSurface({ initialOpen }: {
|
|
18
|
+
initialOpen: boolean;
|
|
19
|
+
}, { resetKey }?: {
|
|
20
|
+
resetKey?: number | undefined;
|
|
21
|
+
}): {
|
|
22
|
+
open: boolean;
|
|
23
|
+
toggle: () => void;
|
|
24
|
+
panelRef: RefObject<HTMLDivElement | null>;
|
|
25
|
+
launcherRef: RefObject<HTMLDivElement | null>;
|
|
26
|
+
panelId: string;
|
|
27
|
+
};
|
|
28
|
+
//# sourceMappingURL=dialog.hooks.d.ts.map
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { useCallback, useEffect, useId, useLayoutEffect, useRef, useState } from 'react';
|
|
2
|
+
import { useFocusTrap } from '@jini-ai/ui/panel-kit';
|
|
3
|
+
/**
|
|
4
|
+
* Shares the panel-kit trap across embeds. Escape respects a child popup's preventDefault;
|
|
5
|
+
* Closing restores the mounted launcher, including after a lazy-loader handoff.
|
|
6
|
+
* A changed resetKey focuses the replacement composer after the host remounts the pane.
|
|
7
|
+
* @complexity O(focusable controls) per Tab; O(1) listeners and retained state.
|
|
8
|
+
*/
|
|
9
|
+
export function useEmbedChatDialog({ open, onClose, panelRef, launcherRef }, { resetKey } = {}) {
|
|
10
|
+
const wasOpen = useRef(open);
|
|
11
|
+
const previousResetKey = useRef(resetKey);
|
|
12
|
+
useFocusTrap({ containerRef: panelRef }, { active: open });
|
|
13
|
+
useLayoutEffect(() => {
|
|
14
|
+
if (open && previousResetKey.current !== resetKey) {
|
|
15
|
+
panelRef.current?.querySelector('textarea:not(:disabled)')?.focus();
|
|
16
|
+
}
|
|
17
|
+
previousResetKey.current = resetKey;
|
|
18
|
+
}, [open, resetKey, panelRef]);
|
|
19
|
+
useEffect(() => {
|
|
20
|
+
if (wasOpen.current && !open) {
|
|
21
|
+
const launcher = launcherRef.current;
|
|
22
|
+
(launcher?.querySelector('button') ?? launcher)?.focus();
|
|
23
|
+
}
|
|
24
|
+
wasOpen.current = open;
|
|
25
|
+
}, [open, launcherRef]);
|
|
26
|
+
useEffect(() => {
|
|
27
|
+
if (!open)
|
|
28
|
+
return;
|
|
29
|
+
const panel = panelRef.current;
|
|
30
|
+
const view = panel?.ownerDocument.defaultView;
|
|
31
|
+
if (!panel || !view)
|
|
32
|
+
return;
|
|
33
|
+
const onKeyDown = (event) => {
|
|
34
|
+
if (event.key === 'Escape' && !event.defaultPrevented) {
|
|
35
|
+
event.preventDefault();
|
|
36
|
+
onClose();
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
view.addEventListener('keydown', onKeyDown);
|
|
40
|
+
return () => view.removeEventListener('keydown', onKeyDown);
|
|
41
|
+
}, [open, onClose, panelRef]);
|
|
42
|
+
}
|
|
43
|
+
/** Reusable state/ref wiring for the packaged embed. O(1) besides the shared trap's Tab scan. */
|
|
44
|
+
export function useEmbedChatSurface({ initialOpen }, { resetKey } = {}) {
|
|
45
|
+
const [open, setOpen] = useState(initialOpen);
|
|
46
|
+
const panelRef = useRef(null);
|
|
47
|
+
const launcherRef = useRef(null);
|
|
48
|
+
const panelId = useId();
|
|
49
|
+
const close = useCallback(() => setOpen(false), []);
|
|
50
|
+
const toggle = useCallback(() => setOpen(current => !current), []);
|
|
51
|
+
useEmbedChatDialog({ open, onClose: close, panelRef, launcherRef }, { resetKey });
|
|
52
|
+
return { open, toggle, panelRef, launcherRef, panelId };
|
|
53
|
+
}
|
|
54
|
+
//# sourceMappingURL=dialog.hooks.js.map
|
|
@@ -2,6 +2,7 @@ import type { ChatTransport } from '../../core/transport.js';
|
|
|
2
2
|
import type { ChatPaneAgent } from '../../react/chat-pane.js';
|
|
3
3
|
import type { BrowserSessionStore } from '../browser/session-store.js';
|
|
4
4
|
import type { PageActionPort } from '../../core/browser/page-actions.js';
|
|
5
|
+
export { useEmbedChatDialog } from './dialog.hooks.js';
|
|
5
6
|
import type { EmbedHeaderCopy, EscapeKeyPort } from './header.js';
|
|
6
7
|
export { EmbedChatHeader } from './header.js';
|
|
7
8
|
export type { EmbedChatHeaderArgs, EmbedHeaderCopy, EscapeKeyPort } from './header.js';
|
|
@@ -7,6 +7,8 @@ import { ChatFab } from '../../react/components/ChatFab.js';
|
|
|
7
7
|
import { JiniChatProvider } from '../../react/components/JiniChatProvider.js';
|
|
8
8
|
import { extractPageActions, splitPageActions, isQueuedPageAction } from '../../core/browser/page-actions.js';
|
|
9
9
|
import { EmbedChatHeader } from './header.js';
|
|
10
|
+
import { useEmbedChatSurface } from './dialog.hooks.js';
|
|
11
|
+
export { useEmbedChatDialog } from './dialog.hooks.js';
|
|
10
12
|
export { EmbedChatHeader } from './header.js';
|
|
11
13
|
const shellStyles = `
|
|
12
14
|
.jini-chat-embed__panel { position:fixed; right:var(--jini-embed-right,24px); bottom:var(--jini-embed-bottom,88px); width:min(var(--jini-embed-width,440px),calc(100vw - 32px)); height:min(var(--jini-embed-height,620px),calc(100dvh - 120px)); z-index:var(--jini-embed-z-index,1000); display:flex; flex-direction:column; }
|
|
@@ -25,9 +27,9 @@ const shellStyles = `
|
|
|
25
27
|
export function EmbedChatWidget(args) {
|
|
26
28
|
const { agent, copy, transport, session, pageActions, directiveEventName, keyboard, scheduleFrame } = args;
|
|
27
29
|
const [initial] = useState(() => session.load({}));
|
|
28
|
-
const [open, setOpen] = useState(initial.open);
|
|
29
|
-
const [messages, setMessages] = useState(initial.messages);
|
|
30
30
|
const [paneKey, setPaneKey] = useState(0);
|
|
31
|
+
const { open, toggle, panelRef, launcherRef, panelId } = useEmbedChatSurface({ initialOpen: initial.open }, { resetKey: paneKey });
|
|
32
|
+
const [messages, setMessages] = useState(initial.messages);
|
|
31
33
|
const [pending, setPending] = useState(null);
|
|
32
34
|
const processed = useRef(new Set(initial.messages.map(message => message.id)));
|
|
33
35
|
useEffect(() => { session.save({ state: { open, messages } }); }, [session, open, messages]);
|
|
@@ -76,7 +78,7 @@ export function EmbedChatWidget(args) {
|
|
|
76
78
|
else
|
|
77
79
|
setPending(proposal);
|
|
78
80
|
}, [directiveEventName, pageActions, go]);
|
|
79
|
-
return _jsxs("div", { className: "jini-chat-embed", children: [_jsx("style", { children: shellStyles }), _jsx(ChatFab, { open: open, onToggle:
|
|
81
|
+
return _jsxs("div", { className: "jini-chat-embed", children: [_jsx("style", { children: shellStyles }), _jsx("div", { ref: launcherRef, tabIndex: -1, children: _jsx(ChatFab, { open: open, onToggle: toggle, "aria-label": copy.fabLabel, label: copy.fabLabel, "aria-controls": panelId, "aria-haspopup": "dialog" }) }), _jsx(JiniChatProvider, { transport: transport, children: _jsxs("div", { id: panelId, className: "jini-chat-embed__panel", hidden: !open, ref: panelRef, role: "dialog", "aria-modal": "true", "aria-label": copy.title, children: [_jsx(ChatPane, { transport: transport, agents: [agent], initialSelection: { agentId: agent.id }, initialMessages: messages, title: copy.title, placeholder: copy.placeholder, variant: "embed", header: _jsx(EmbedChatHeader, { title: copy.title, hasMessages: messages.length > 0, onReset: reset, copy: copy, keyboard: keyboard }), onMessagesChange: onMessagesChange }, paneKey), pending ? _jsxs("div", { className: "jini-chat-embed__proposal", children: [_jsx("span", { children: copy.proposalLabel({ title: pending.navigate.target.title }) }), _jsx("button", { type: "button", onClick: () => { setPending(null); go(pending, messages); }, children: copy.proposalGo })] }) : null] }) })] });
|
|
80
82
|
}
|
|
81
83
|
/** Mounts into a host-provided node and returns lifecycle cleanup. O(1) plus React rendering. */
|
|
82
84
|
export function mountEmbedChat({ element, widget }) {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
1
2
|
export interface EmbedHeaderCopy {
|
|
2
3
|
readonly eyebrow: string;
|
|
3
4
|
readonly newThread: string;
|
|
@@ -15,7 +16,8 @@ export interface EmbedChatHeaderArgs {
|
|
|
15
16
|
readonly onReset: () => void;
|
|
16
17
|
readonly copy: EmbedHeaderCopy;
|
|
17
18
|
readonly keyboard: EscapeKeyPort;
|
|
19
|
+
readonly closeAction?: ReactNode;
|
|
18
20
|
}
|
|
19
21
|
/** Two-step reset with safe initial focus, Cancel and host-wide Escape. O(1) state and listeners. */
|
|
20
|
-
export declare function EmbedChatHeader({ title, hasMessages, onReset, copy, keyboard }: EmbedChatHeaderArgs): import("react").JSX.Element;
|
|
22
|
+
export declare function EmbedChatHeader({ title, hasMessages, onReset, copy, keyboard, closeAction }: EmbedChatHeaderArgs): import("react").JSX.Element;
|
|
21
23
|
//# sourceMappingURL=header.d.ts.map
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { EmbedChatHeaderArgs } from './header.js';
|
|
2
|
+
/** Confirm discarding messages or a live draft; Cancel/Escape restore the reset button.
|
|
3
|
+
* @complexity O(pane controls + draft length) per request; O(1) retained state.
|
|
4
|
+
*/
|
|
5
|
+
export declare function useEmbedChatHeader({ hasMessages, onReset, keyboard }: Pick<EmbedChatHeaderArgs, 'hasMessages' | 'onReset' | 'keyboard'>, _options?: {}): {
|
|
6
|
+
confirming: boolean;
|
|
7
|
+
cancel: () => void;
|
|
8
|
+
headerRef: import("react").RefObject<HTMLDivElement | null>;
|
|
9
|
+
resetButtonRef: import("react").RefObject<HTMLButtonElement | null>;
|
|
10
|
+
requestReset: () => void;
|
|
11
|
+
confirmReset: () => void;
|
|
12
|
+
};
|
|
13
|
+
//# sourceMappingURL=header.hooks.d.ts.map
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { useEffect, useLayoutEffect, useRef, useState } from 'react';
|
|
2
|
+
/** Confirm discarding messages or a live draft; Cancel/Escape restore the reset button.
|
|
3
|
+
* @complexity O(pane controls + draft length) per request; O(1) retained state.
|
|
4
|
+
*/
|
|
5
|
+
export function useEmbedChatHeader({ hasMessages, onReset, keyboard }, _options = {}) {
|
|
6
|
+
const [confirming, setConfirming] = useState(false);
|
|
7
|
+
const headerRef = useRef(null);
|
|
8
|
+
const resetButtonRef = useRef(null);
|
|
9
|
+
const wasConfirming = useRef(false);
|
|
10
|
+
const cancel = () => setConfirming(false);
|
|
11
|
+
useLayoutEffect(() => {
|
|
12
|
+
if (wasConfirming.current && !confirming)
|
|
13
|
+
resetButtonRef.current?.focus();
|
|
14
|
+
wasConfirming.current = confirming;
|
|
15
|
+
}, [confirming]);
|
|
16
|
+
useEffect(() => {
|
|
17
|
+
if (!confirming)
|
|
18
|
+
return;
|
|
19
|
+
return keyboard.subscribe({ onEscape: () => setConfirming(false) });
|
|
20
|
+
}, [confirming, keyboard]);
|
|
21
|
+
return { confirming, cancel, headerRef, resetButtonRef,
|
|
22
|
+
requestReset: () => {
|
|
23
|
+
// The pane owns the draft; inspect its live composer rather than keeping a second copy.
|
|
24
|
+
const composer = headerRef.current?.closest('.jini-chat-pane')?.querySelector('.jini-composer-input');
|
|
25
|
+
if (hasMessages || composer?.value)
|
|
26
|
+
setConfirming(true);
|
|
27
|
+
else
|
|
28
|
+
onReset();
|
|
29
|
+
},
|
|
30
|
+
confirmReset: () => { setConfirming(false); onReset(); }, };
|
|
31
|
+
}
|
|
32
|
+
//# sourceMappingURL=header.hooks.js.map
|
|
@@ -1,18 +1,8 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import {
|
|
2
|
+
import { useEmbedChatHeader } from './header.hooks.js';
|
|
3
3
|
/** Two-step reset with safe initial focus, Cancel and host-wide Escape. O(1) state and listeners. */
|
|
4
|
-
export function EmbedChatHeader({ title, hasMessages, onReset, copy, keyboard }) {
|
|
5
|
-
const
|
|
6
|
-
|
|
7
|
-
if (!confirming)
|
|
8
|
-
return;
|
|
9
|
-
return keyboard.subscribe({ onEscape: () => setConfirming(false) });
|
|
10
|
-
}, [confirming, keyboard]);
|
|
11
|
-
return _jsxs("div", { className: "jini-chat-pane__header", children: [_jsxs("div", { className: "jini-chat-pane__heading", children: [_jsx("span", { className: "jini-chat-pane__eyebrow", children: copy.eyebrow }), _jsx("h2", { className: "jini-chat-pane__title", children: title })] }), confirming ? _jsxs("div", { className: "jini-chat-embed__reset-confirm", children: [_jsx("button", { type: "button", className: "jini-chat-embed__reset-cancel", autoFocus: true, onClick: () => setConfirming(false), children: copy.cancel }), _jsx("button", { type: "button", className: "jini-chat-pane__new-thread", onClick: () => { setConfirming(false); onReset(); }, children: copy.discard })] }) : _jsx("button", { type: "button", className: "jini-chat-pane__new-thread", onClick: () => {
|
|
12
|
-
if (hasMessages)
|
|
13
|
-
setConfirming(true);
|
|
14
|
-
else
|
|
15
|
-
onReset();
|
|
16
|
-
}, children: copy.newThread })] });
|
|
4
|
+
export function EmbedChatHeader({ title, hasMessages, onReset, copy, keyboard, closeAction }) {
|
|
5
|
+
const { confirming, cancel, requestReset, confirmReset, headerRef, resetButtonRef } = useEmbedChatHeader({ hasMessages, onReset, keyboard });
|
|
6
|
+
return _jsxs("div", { className: "jini-chat-pane__header", ref: headerRef, children: [_jsxs("div", { className: "jini-chat-pane__heading", children: [copy.eyebrow && _jsx("span", { className: "jini-chat-pane__eyebrow", children: copy.eyebrow }), _jsx("h2", { className: "jini-chat-pane__title", children: title })] }), confirming ? _jsxs("div", { className: "jini-chat-embed__reset-confirm", children: [_jsx("button", { type: "button", className: "jini-chat-embed__reset-cancel", autoFocus: true, onClick: cancel, children: copy.cancel }), _jsx("button", { type: "button", className: "jini-chat-pane__new-thread", onClick: confirmReset, children: copy.discard })] }) : _jsx("button", { type: "button", className: "jini-chat-pane__new-thread", ref: resetButtonRef, onClick: requestReset, children: copy.newThread }), closeAction] });
|
|
17
7
|
}
|
|
18
8
|
//# sourceMappingURL=header.js.map
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import type { VanillaChatControllerArgs, VanillaChatState } from './state.js';
|
|
2
|
+
export { createVanillaChatController } from './state.js';
|
|
3
|
+
export type { VanillaChatControllerArgs, VanillaChatState } from './state.js';
|
|
4
|
+
export interface VanillaChatCopy {
|
|
5
|
+
readonly title: string;
|
|
6
|
+
readonly fabLabel: string;
|
|
7
|
+
readonly placeholder: string;
|
|
8
|
+
readonly send: string;
|
|
9
|
+
readonly stop: string;
|
|
10
|
+
readonly close: string;
|
|
11
|
+
readonly reset: string;
|
|
12
|
+
readonly resetQuestion: string;
|
|
13
|
+
readonly confirm: string;
|
|
14
|
+
readonly confirmDraft?: string;
|
|
15
|
+
readonly keep: string;
|
|
16
|
+
readonly proposalGo: string;
|
|
17
|
+
readonly proposalLabel: (args: {
|
|
18
|
+
title: string;
|
|
19
|
+
}) => string;
|
|
20
|
+
readonly thinking: string;
|
|
21
|
+
readonly greeting?: string;
|
|
22
|
+
readonly suggestionsLabel?: string;
|
|
23
|
+
}
|
|
24
|
+
export interface MountVanillaChatArgs extends Omit<VanillaChatControllerArgs, 'onChange'> {
|
|
25
|
+
readonly element: HTMLElement;
|
|
26
|
+
readonly document: Document;
|
|
27
|
+
readonly copy: VanillaChatCopy;
|
|
28
|
+
readonly suggestions?: readonly string[];
|
|
29
|
+
/** Host renderer must escape raw HTML and reject unsafe link protocols. */
|
|
30
|
+
readonly renderMarkdown: (args: {
|
|
31
|
+
content: string;
|
|
32
|
+
element: HTMLElement;
|
|
33
|
+
}) => void;
|
|
34
|
+
readonly scheduleFrame: (args: {
|
|
35
|
+
callback: () => void;
|
|
36
|
+
}) => () => void;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Mounts a DOM-only chat panel. Native buttons/forms supply keyboard semantics;
|
|
40
|
+
* explicitly opened dialogs trap focus, restore the bubble on close and announce settled replies.
|
|
41
|
+
* Restored panels leave page focus alone. Streaming markdown is rendered at most once per frame.
|
|
42
|
+
* All listeners, scheduled page effects and runs belong to this mount and are disposed.
|
|
43
|
+
* Markdown is an injected host capability; no framework or icon font enters this graph.
|
|
44
|
+
*/
|
|
45
|
+
export declare function mountVanillaChat(args: MountVanillaChatArgs, options?: {
|
|
46
|
+
initialOpen?: boolean;
|
|
47
|
+
}): {
|
|
48
|
+
controller: {
|
|
49
|
+
getState(_input?: Record<string, never>, _opts?: {}): VanillaChatState;
|
|
50
|
+
setOpen({ open }: {
|
|
51
|
+
open: boolean;
|
|
52
|
+
}, _opts?: {}): void;
|
|
53
|
+
send({ text }: {
|
|
54
|
+
text: string;
|
|
55
|
+
}, _opts?: {}): Promise<boolean>;
|
|
56
|
+
acceptProposal(_input?: Record<string, never>, _opts?: {}): void;
|
|
57
|
+
cancel(_input?: Record<string, never>, _opts?: {}): void;
|
|
58
|
+
reset(_input?: Record<string, never>, _opts?: {}): void;
|
|
59
|
+
dispose(_input?: Record<string, never>, _opts?: {}): void;
|
|
60
|
+
};
|
|
61
|
+
unmount(_input?: Record<string, never>, _opts?: {}): void;
|
|
62
|
+
};
|
|
63
|
+
//# sourceMappingURL=index.d.ts.map
|