@frontera-sdk/chat 1.52.45 → 1.52.47

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 (51) hide show
  1. package/dist/auth.d.ts +20 -0
  2. package/dist/auth.js +11 -0
  3. package/dist/callback-page.d.ts +49 -0
  4. package/dist/callback-page.js +84 -0
  5. package/dist/chat-client.d.ts +221 -0
  6. package/dist/chat-client.js +5 -0
  7. package/dist/chat-errors.d.ts +30 -0
  8. package/dist/chat-errors.js +3 -0
  9. package/dist/chunk-CMMWTCYS.js +116 -0
  10. package/dist/chunk-KZEGEV3P.js +35 -0
  11. package/dist/chunk-LS26G2N6.js +120 -0
  12. package/dist/chunk-RLHHRJWS.js +371 -0
  13. package/dist/chunk-UCUVHMJC.js +62 -0
  14. package/dist/chunk-VWUR3ECR.js +115 -0
  15. package/dist/chunk-W25Q2DQA.js +32 -0
  16. package/dist/chunk-YKQ5Z5EK.js +32 -0
  17. package/dist/connect-popup.d.ts +150 -0
  18. package/dist/connect-popup.js +138 -0
  19. package/dist/doctor.d.ts +71 -0
  20. package/dist/doctor.js +3 -0
  21. package/dist/element.d.ts +23 -0
  22. package/dist/element.js +175 -0
  23. package/dist/frontera-chat.iife.js +27 -0
  24. package/dist/hooks.d.ts +88 -0
  25. package/dist/hooks.js +3 -0
  26. package/dist/provider.d.ts +20 -0
  27. package/dist/provider.js +13 -0
  28. package/dist/public-chat.d.ts +50 -0
  29. package/dist/public-chat.js +6 -0
  30. package/dist/public-token.d.ts +46 -0
  31. package/dist/public-token.js +3 -0
  32. package/dist/reply-text.d.ts +25 -0
  33. package/dist/reply-text.js +3 -0
  34. package/dist/stream.d.ts +25 -0
  35. package/dist/stream.js +3 -0
  36. package/dist/token-session.d.ts +66 -0
  37. package/dist/token-session.js +99 -0
  38. package/dist/transport.d.ts +60 -0
  39. package/dist/transport.js +103 -0
  40. package/dist/types.d.ts +361 -0
  41. package/dist/types.js +3 -0
  42. package/dist/ui-stream.d.ts +25 -0
  43. package/dist/ui-stream.js +26 -0
  44. package/package.json +70 -30
  45. package/src/chat-errors.ts +43 -0
  46. package/src/element.ts +201 -0
  47. package/src/frontera-chat.global.ts +13 -0
  48. package/src/public-chat.ts +154 -0
  49. package/src/public-token.ts +36 -4
  50. package/src/reply-text.ts +49 -0
  51. package/src/transport.ts +7 -1
@@ -0,0 +1,32 @@
1
+ // src/reply-text.ts
2
+ var CITATION = /\s?\[([^\]]*)\]\(\/cite\/[^)\s]*\)/g;
3
+ function withoutCitations(text) {
4
+ return text.replace(CITATION, (match, label) => /^\s*\d+\s*$/.test(label) ? "" : match.startsWith(" ") ? ` ${label}` : label);
5
+ }
6
+ function safeHref(href) {
7
+ return /^(https?:\/\/|mailto:)/i.test(href.trim()) ? href.trim() : null;
8
+ }
9
+ var INLINE = /\*\*([^*]+)\*\*|\[([^\]]+)\]\(([^)\s]+)\)/g;
10
+ function pieces(line) {
11
+ const out = [];
12
+ let at = 0;
13
+ for (const m of line.matchAll(INLINE)) {
14
+ if (m.index > at) out.push({ kind: "text", text: line.slice(at, m.index) });
15
+ if (m[1] !== void 0) out.push({ kind: "bold", text: m[1] });
16
+ else {
17
+ const href = safeHref(m[3]);
18
+ out.push(href ? { kind: "link", text: m[2], href } : { kind: "text", text: m[2] });
19
+ }
20
+ at = m.index + m[0].length;
21
+ }
22
+ if (at < line.length) out.push({ kind: "text", text: line.slice(at) });
23
+ return out;
24
+ }
25
+ function replyParagraphs(text, options = {}) {
26
+ const shown = options.showSources ? text : withoutCitations(text);
27
+ return shown.split(/\n\s*\n/).map((p) => p.trim()).filter(Boolean).map((p) => p.split("\n").map((line) => pieces(line.replace(/^\s*[-*]\s+/, "\u2022 "))));
28
+ }
29
+
30
+ export { replyParagraphs, withoutCitations };
31
+ //# sourceMappingURL=chunk-W25Q2DQA.js.map
32
+ //# sourceMappingURL=chunk-W25Q2DQA.js.map
@@ -0,0 +1,32 @@
1
+ import { FronteraError } from '@frontera-sdk/core/errors';
2
+
3
+ // src/chat-errors.ts
4
+ var CHAT_ERROR_CODES = {
5
+ UNAUTHORIZED: "The key or token was not accepted: it is disabled, removed, mistyped or expired. Mint a new token; if that fails too, check the key.",
6
+ FORBIDDEN: "The credential may not do this: the page is not on the key\u2019s allowed websites, or a visitor asked for something a public chat cannot do.",
7
+ NOT_FOUND: "The conversation or run is not this credential\u2019s, or no longer exists.",
8
+ VALIDATION_ERROR: "The request was malformed, for example an empty message or more than five messages.",
9
+ RATE_LIMITED: "Too many messages or new chats in a short time. Wait the number of seconds the message names, then try again.",
10
+ QUOTA_EXCEEDED: "The organization\u2019s usage limit is reached. An administrator raises it.",
11
+ SERVICE_UNAVAILABLE: "The platform can\u2019t take the request right now. Try again shortly.",
12
+ INTERNAL_ERROR: "Something failed on the platform. Try again; if it keeps failing, the message names what to report.",
13
+ PROTOCOL_ERROR: "The response wasn\u2019t a chat stream: the API origin is likely wrong, or a proxy changed the response.",
14
+ FORBIDDEN_LOCAL: "Your own `authorizeConversation` callback denied access to the conversation.",
15
+ CONFIG_MISSING: "The client has no API origin or credential."
16
+ };
17
+ function chatErrorCode(err) {
18
+ const code = err instanceof FronteraError ? err.code : null;
19
+ return code && code in CHAT_ERROR_CODES ? code : "INTERNAL_ERROR";
20
+ }
21
+ function visitorErrorWords(err) {
22
+ const code = chatErrorCode(err);
23
+ if (code === "RATE_LIMITED" && err instanceof FronteraError) return err.message;
24
+ if (code === "UNAUTHORIZED") return "This chat stopped working. Reload the page to start again.";
25
+ if (code === "FORBIDDEN") return "This chat isn\u2019t available on this page.";
26
+ if (code === "QUOTA_EXCEEDED" || code === "SERVICE_UNAVAILABLE") return "The assistant is unavailable right now. Please try again later.";
27
+ return "The message couldn\u2019t be sent. Please try again.";
28
+ }
29
+
30
+ export { CHAT_ERROR_CODES, chatErrorCode, visitorErrorWords };
31
+ //# sourceMappingURL=chunk-YKQ5Z5EK.js.map
32
+ //# sourceMappingURL=chunk-YKQ5Z5EK.js.map
@@ -0,0 +1,150 @@
1
+ import { ChatClient } from './chat-client.js';
2
+ import { ResumeToolCallResult } from './types.js';
3
+ import '@frontera-sdk/core/client';
4
+ import './stream.js';
5
+
6
+ /**
7
+ * The browser half of connecting an end user's own account.
8
+ *
9
+ * The server side of this flow is three calls (`startAppConnection`,
10
+ * `completeAppConnection`, `resumeToolCall`) with a consent screen in the
11
+ * middle. What is fiddly is not the calls — it is the middle: opening a popup,
12
+ * knowing when it came back, telling "the user finished" apart from "the user
13
+ * closed the window", and not leaving a listener attached forever when neither
14
+ * happens.
15
+ *
16
+ * That is what lives here. It is deliberately not part of `ChatClient`: the
17
+ * client is transport, and this touches `window`.
18
+ */
19
+
20
+ /** What a callback page sends back to the window that opened it. */
21
+ declare const CONNECT_MESSAGE_TYPE = "frontera:oauth-callback";
22
+ interface ConnectCallbackMessage {
23
+ type: typeof CONNECT_MESSAGE_TYPE;
24
+ code?: string;
25
+ state?: string;
26
+ /** The provider's own refusal — the user pressed Cancel, typically. */
27
+ error?: string;
28
+ }
29
+ /**
30
+ * Call this FROM YOUR CALLBACK PAGE — the one you registered as the OAuth
31
+ * redirect URI. It is the page's entire implementation:
32
+ *
33
+ * ```ts
34
+ * // app/frontera/oauth/callback/page.tsx
35
+ * import { postConnectResultToOpener } from '@frontera-sdk/chat/connect-popup'
36
+ * postConnectResultToOpener(window.location.search)
37
+ * ```
38
+ *
39
+ * It handles both ways a connect can arrive back. In a popup it posts to the
40
+ * opener and closes. In a redirected tab — which is what mobile gets, because
41
+ * popups are blocked or mangled in iOS Safari and in-app browsers — there is no
42
+ * opener, so it navigates back to where the chat was with the result attached.
43
+ *
44
+ * `targetOrigin` defaults to this page's own origin, correct when the chat and
45
+ * the callback are the same app. Never `'*'`: the message carries an
46
+ * authorization code.
47
+ */
48
+ declare function postConnectResultToOpener(search: string, targetOrigin?: string): void;
49
+ interface RunAppConnectionOptions {
50
+ /**
51
+ * The parked call's id — what gets resumed once the account exists.
52
+ *
53
+ * Omit for a VOLUNTARY connect: someone connecting an account from the
54
+ * composer before asking anything has no parked call, and there is nothing
55
+ * to resume. The account still lands the same way; only the last step
56
+ * differs.
57
+ */
58
+ pendingToolCallId?: string;
59
+ /** Which app. Take `installId` from the parked call. */
60
+ installId?: string;
61
+ catalogKind?: string;
62
+ /** Your callback page, registered with your OAuth client AND allowlisted. */
63
+ redirectUri: string;
64
+ /**
65
+ * Origin the callback message must come from. Defaults to `redirectUri`'s
66
+ * origin, which is where your callback page runs.
67
+ */
68
+ expectedOrigin?: string;
69
+ /** How long to wait for the user before giving up. Default 5 minutes. */
70
+ timeoutMs?: number;
71
+ /** Popup geometry, if the defaults do not suit your layout. */
72
+ features?: string;
73
+ /**
74
+ * How to send the person to the provider.
75
+ *
76
+ * `auto` (default) tries a popup and falls back to redirecting this tab when
77
+ * the browser refuses one — which is the normal case on mobile: iOS Safari
78
+ * and in-app browsers (Instagram, LinkedIn, Slack) block or mangle
79
+ * `window.open`. A popup-only integration has a dead Connect button for
80
+ * every one of those users.
81
+ *
82
+ * `redirect` always navigates. `popup` never does, and reports the block.
83
+ */
84
+ mode?: 'auto' | 'popup' | 'redirect';
85
+ /**
86
+ * Where to come back to after a redirect. Defaults to the current URL, so
87
+ * the person lands where they left. Its ORIGIN must be on the workspace's
88
+ * embed allowlist.
89
+ */
90
+ returnTo?: string;
91
+ }
92
+ type RunAppConnectionResult =
93
+ /**
94
+ * The popup path completed here.
95
+ *
96
+ * `resume` is absent when nothing was parked — the plugins button connects
97
+ * an account with no tool call waiting on it, and the connect was the whole
98
+ * errand. Present only when a parked call was released by this connection.
99
+ */
100
+ {
101
+ status: 'connected';
102
+ resume?: ResumeToolCallResult;
103
+ } | {
104
+ status: 'cancelled';
105
+ reason: 'closed' | 'denied' | 'timeout';
106
+ error?: string;
107
+ }
108
+ /**
109
+ * The tab is navigating to the provider. Nothing after this call runs — the
110
+ * flow continues in `completeRedirectedConnection` when the person comes
111
+ * back.
112
+ */
113
+ | {
114
+ status: 'redirecting';
115
+ };
116
+ /**
117
+ * Run the whole thing: open the consent popup, wait for your callback to post
118
+ * the code back, exchange it, and re-run the parked tool call.
119
+ *
120
+ * Resolves rather than throwing when the person simply declines — a closed
121
+ * popup is an ordinary outcome and reads badly as an exception. Genuine
122
+ * failures (a refused start, a rejected exchange) still throw `FronteraError`
123
+ * with the server's code.
124
+ *
125
+ * The resume is included on purpose. Connecting without it leaves the agent
126
+ * exactly as blocked as before: it is waiting on a tool RESULT, not on an
127
+ * account.
128
+ */
129
+ declare function runAppConnection(chat: ChatClient, options: RunAppConnectionOptions): Promise<RunAppConnectionResult>;
130
+ /**
131
+ * Finish a connect that came back as a page load rather than a popup message.
132
+ *
133
+ * Call it once when your chat mounts. It does nothing — cheaply, synchronously
134
+ * — unless this page load is a return from a provider, so it is safe to call
135
+ * unconditionally:
136
+ *
137
+ * ```ts
138
+ * useEffect(() => { void completeRedirectedConnection(chat) }, [chat])
139
+ * ```
140
+ *
141
+ * Without it the redirect path stops one step short: the account gets
142
+ * connected and the parked tool call is never re-run, which looks exactly like
143
+ * the freeze the whole flow exists to prevent. It also tidies the query string,
144
+ * so a refresh does not try to redeem a code that is already spent.
145
+ */
146
+ declare function completeRedirectedConnection(chat: ChatClient, options?: {
147
+ search?: string;
148
+ }): Promise<RunAppConnectionResult | null>;
149
+
150
+ export { CONNECT_MESSAGE_TYPE, type ConnectCallbackMessage, type RunAppConnectionOptions, type RunAppConnectionResult, completeRedirectedConnection, postConnectResultToOpener, runAppConnection };
@@ -0,0 +1,138 @@
1
+ // src/connect-popup.ts
2
+ var CONNECT_MESSAGE_TYPE = "frontera:oauth-callback";
3
+ var PENDING_KEY = "frontera:connect-pending";
4
+ var RETURN_CODE = "frontera_code";
5
+ var RETURN_STATE = "frontera_state";
6
+ var RETURN_ERROR = "frontera_error";
7
+ function postConnectResultToOpener(search, targetOrigin) {
8
+ const params = new URLSearchParams(search);
9
+ const code = params.get("code") ?? void 0;
10
+ const state = params.get("state") ?? void 0;
11
+ const error = params.get("error") ?? void 0;
12
+ if (window.opener) {
13
+ const message = { type: CONNECT_MESSAGE_TYPE, code, state, error };
14
+ window.opener.postMessage(message, targetOrigin ?? window.location.origin);
15
+ window.close();
16
+ return;
17
+ }
18
+ const pending = readPending();
19
+ const target = new URL(pending?.returnTo ?? window.location.origin);
20
+ if (code) target.searchParams.set(RETURN_CODE, code);
21
+ if (state) target.searchParams.set(RETURN_STATE, state);
22
+ if (error) target.searchParams.set(RETURN_ERROR, error);
23
+ window.location.replace(target.toString());
24
+ }
25
+ function readPending() {
26
+ try {
27
+ const raw = window.sessionStorage.getItem(PENDING_KEY);
28
+ return raw ? JSON.parse(raw) : null;
29
+ } catch {
30
+ return null;
31
+ }
32
+ }
33
+ function writePending(value) {
34
+ try {
35
+ if (value) window.sessionStorage.setItem(PENDING_KEY, JSON.stringify(value));
36
+ else window.sessionStorage.removeItem(PENDING_KEY);
37
+ } catch {
38
+ }
39
+ }
40
+ var DEFAULT_TIMEOUT_MS = 5 * 6e4;
41
+ var DEFAULT_FEATURES = "width=520,height=680,menubar=no,toolbar=no";
42
+ async function runAppConnection(chat, options) {
43
+ const mode = options.mode ?? "auto";
44
+ const returnTo = options.returnTo ?? window.location.href;
45
+ const { authUrl } = await chat.startAppConnection({
46
+ installId: options.installId,
47
+ catalogKind: options.catalogKind,
48
+ redirectUri: options.redirectUri,
49
+ returnTo
50
+ });
51
+ const expectedOrigin = options.expectedOrigin ?? new URL(options.redirectUri).origin;
52
+ const goByRedirect = () => {
53
+ writePending({ pendingToolCallId: options.pendingToolCallId ?? null, returnTo });
54
+ window.location.assign(authUrl);
55
+ return { status: "redirecting" };
56
+ };
57
+ if (mode === "redirect") return goByRedirect();
58
+ const popup = window.open(authUrl, "frontera-connect", options.features ?? DEFAULT_FEATURES);
59
+ if (!popup) {
60
+ if (mode === "auto") return goByRedirect();
61
+ throw new Error(
62
+ "The connection window was blocked. Open it from a direct click, or allow popups for this site."
63
+ );
64
+ }
65
+ const outcome = await waitForCallback(popup, expectedOrigin, options.timeoutMs ?? DEFAULT_TIMEOUT_MS);
66
+ if (outcome.status !== "received") return { status: "cancelled", reason: outcome.reason, error: outcome.error };
67
+ await chat.completeAppConnection({ code: outcome.code, state: outcome.state });
68
+ if (!options.pendingToolCallId) return { status: "connected" };
69
+ const resume = await chat.resumeToolCall(options.pendingToolCallId);
70
+ return { status: "connected", resume };
71
+ }
72
+ async function completeRedirectedConnection(chat, options = {}) {
73
+ if (typeof window === "undefined") return null;
74
+ const params = new URLSearchParams(options.search ?? window.location.search);
75
+ const code = params.get(RETURN_CODE);
76
+ const state = params.get(RETURN_STATE);
77
+ const error = params.get(RETURN_ERROR);
78
+ if (!code && !state && !error) return null;
79
+ const pending = readPending();
80
+ writePending(null);
81
+ clearReturnParams(params);
82
+ if (error || !code || !state) {
83
+ return { status: "cancelled", reason: "denied", error: error ?? "callback carried no code" };
84
+ }
85
+ if (!pending) {
86
+ await chat.completeAppConnection({ code, state });
87
+ return { status: "connected", resume: { ok: true } };
88
+ }
89
+ await chat.completeAppConnection({ code, state });
90
+ if (!pending.pendingToolCallId) return { status: "connected" };
91
+ const resume = await chat.resumeToolCall(pending.pendingToolCallId);
92
+ return { status: "connected", resume };
93
+ }
94
+ function clearReturnParams(params) {
95
+ params.delete(RETURN_CODE);
96
+ params.delete(RETURN_STATE);
97
+ params.delete(RETURN_ERROR);
98
+ const query = params.toString();
99
+ window.history.replaceState(
100
+ window.history.state,
101
+ "",
102
+ `${window.location.pathname}${query ? `?${query}` : ""}${window.location.hash}`
103
+ );
104
+ }
105
+ function waitForCallback(popup, expectedOrigin, timeoutMs) {
106
+ return new Promise((resolve) => {
107
+ let settled = false;
108
+ const finish = (outcome) => {
109
+ if (settled) return;
110
+ settled = true;
111
+ window.removeEventListener("message", onMessage);
112
+ clearInterval(closeTimer);
113
+ clearTimeout(timeoutTimer);
114
+ resolve(outcome);
115
+ };
116
+ const onMessage = (event) => {
117
+ if (event.origin !== expectedOrigin) return;
118
+ const data = event.data;
119
+ if (!data || data.type !== CONNECT_MESSAGE_TYPE) return;
120
+ if (data.error) return finish({ status: "cancelled", reason: "denied", error: data.error });
121
+ if (!data.code || !data.state) {
122
+ return finish({ status: "cancelled", reason: "denied", error: "callback carried no code" });
123
+ }
124
+ finish({ status: "received", code: data.code, state: data.state });
125
+ };
126
+ window.addEventListener("message", onMessage);
127
+ const closeTimer = setInterval(() => {
128
+ if (popup.closed) finish({ status: "cancelled", reason: "closed" });
129
+ }, 400);
130
+ const timeoutTimer = setTimeout(() => {
131
+ finish({ status: "cancelled", reason: "timeout" });
132
+ }, timeoutMs);
133
+ });
134
+ }
135
+
136
+ export { CONNECT_MESSAGE_TYPE, completeRedirectedConnection, postConnectResultToOpener, runAppConnection };
137
+ //# sourceMappingURL=connect-popup.js.map
138
+ //# sourceMappingURL=connect-popup.js.map
@@ -0,0 +1,71 @@
1
+ /**
2
+ * A development-time watchdog for the ways a chat goes quiet.
3
+ *
4
+ * The failure this exists for looks like nothing at all: the agent stops
5
+ * mid-turn because it is waiting for a person, the integration does not render
6
+ * the card that would let them answer, and the run sits there until it lapses.
7
+ * No error, no rejected promise, no failed request — which is precisely why it
8
+ * reaches production. The first anyone hears of it is a user saying "it froze".
9
+ *
10
+ * So the SDK watches its own stream in development. When something parks and
11
+ * nothing settles it within a grace period, this says so, in the integrator's
12
+ * console, naming the call they are missing. Silence becomes a message at the
13
+ * moment it happens rather than a support ticket a week later.
14
+ *
15
+ * Off in production, always: it tees the response body to read it, and that is
16
+ * a cost worth paying for a warning nobody in production would see.
17
+ */
18
+ type DoctorConcern = 'approval' | 'connect' | 'unauthorized';
19
+ interface ChatDoctorOptions {
20
+ /** Force on or off. Defaults to on outside production. */
21
+ enabled?: boolean;
22
+ /**
23
+ * How long a parked run may go unanswered before it is worth mentioning.
24
+ *
25
+ * Long enough that a UI fetching its pending list on the next tick is never
26
+ * scolded; short enough to land while the developer is still looking at the
27
+ * screen that caused it.
28
+ */
29
+ graceMs?: number;
30
+ /** Where warnings go. Defaults to `console.warn`. */
31
+ onWarn?: (concern: DoctorConcern, message: string) => void;
32
+ }
33
+ declare class ChatDoctor {
34
+ private options;
35
+ private timers;
36
+ configure(options: ChatDoctorOptions): void;
37
+ get enabled(): boolean;
38
+ private warn;
39
+ /**
40
+ * Something parked. Start counting; `resolved` before the grace period ends
41
+ * cancels it.
42
+ */
43
+ parked(concern: DoctorConcern): void;
44
+ /** The integration did the thing. Nothing to warn about. */
45
+ resolved(concern: DoctorConcern): void;
46
+ /** Immediate, not timed: nothing about a missing refresh improves by waiting. */
47
+ noticed(concern: DoctorConcern): void;
48
+ /** For tests, and for a host that unmounts a chat mid-run. */
49
+ reset(): void;
50
+ /**
51
+ * Watch a chat stream for the two things that park a run.
52
+ *
53
+ * The body is teed rather than parsed: this reads a copy as text and matches
54
+ * two markers, leaving the real stream untouched for the AI SDK. A full parse
55
+ * here would be a second implementation of the protocol, kept in step with
56
+ * the first by hope.
57
+ */
58
+ observe(response: Response): Response;
59
+ private scan;
60
+ }
61
+ /**
62
+ * The instance the SDK reports into. Exported so a host can quieten it, widen
63
+ * the grace period, or route warnings into their own logger:
64
+ *
65
+ * ```ts
66
+ * chatDoctor.configure({ enabled: false })
67
+ * ```
68
+ */
69
+ declare const chatDoctor: ChatDoctor;
70
+
71
+ export { type ChatDoctorOptions, type DoctorConcern, chatDoctor };
package/dist/doctor.js ADDED
@@ -0,0 +1,3 @@
1
+ export { chatDoctor } from './chunk-VWUR3ECR.js';
2
+ //# sourceMappingURL=doctor.js.map
3
+ //# sourceMappingURL=doctor.js.map
@@ -0,0 +1,23 @@
1
+ declare const Base: typeof HTMLElement;
2
+ declare class FronteraChatElement extends Base {
3
+ static observedAttributes: string[];
4
+ private chat;
5
+ private log;
6
+ private input;
7
+ private sendButton;
8
+ private header;
9
+ private busy;
10
+ connectedCallback(): void;
11
+ attributeChangedCallback(): void;
12
+ private applyAttributes;
13
+ private setBusy;
14
+ private note;
15
+ private agentBubble;
16
+ /** The reply as paragraphs of text, bold and links, built as nodes: nothing from the reply becomes HTML. */
17
+ private render;
18
+ private submit;
19
+ }
20
+ /** Register the element under `tag` (once); the script-tag build calls it for you. */
21
+ declare function defineFronteraChat(tag?: string): void;
22
+
23
+ export { FronteraChatElement, defineFronteraChat };
@@ -0,0 +1,175 @@
1
+ import { PublicChat } from './chunk-LS26G2N6.js';
2
+ import './chunk-UCUVHMJC.js';
3
+ import { replyParagraphs } from './chunk-W25Q2DQA.js';
4
+ import './chunk-KZEGEV3P.js';
5
+ import { visitorErrorWords } from './chunk-YKQ5Z5EK.js';
6
+
7
+ // src/element.ts
8
+ var STYLE = `
9
+ :host { display: block; height: 100%; font: 14px/1.45 var(--frontera-chat-font, system-ui, sans-serif); color: var(--frontera-chat-text, #1b1b1f); }
10
+ .frame { display: flex; flex-direction: column; height: 100%; min-height: 320px; background: var(--frontera-chat-surface, #fff); border: 1px solid var(--frontera-chat-border, #e3e3e8); border-radius: var(--frontera-chat-radius, 12px); overflow: hidden; }
11
+ header { padding: 12px 16px; font-weight: 600; border-bottom: 1px solid var(--frontera-chat-border, #e3e3e8); }
12
+ .log { flex: 1; overflow-y: auto; padding: 16px; display: flex; flex-direction: column; gap: 10px; }
13
+ .msg { max-width: 85%; padding: 8px 12px; border-radius: 12px; white-space: normal; overflow-wrap: anywhere; }
14
+ .msg p { margin: 0 0 8px; } .msg p:last-child { margin: 0; }
15
+ .agent { align-self: flex-start; background: var(--frontera-chat-agent-bubble, #f1f1f4); }
16
+ .visitor { align-self: flex-end; background: var(--frontera-chat-accent, #2f5bea); color: var(--frontera-chat-accent-text, #fff); }
17
+ .note { align-self: center; font-size: 12px; color: var(--frontera-chat-muted, #6b6b76); }
18
+ .typing { align-self: flex-start; color: var(--frontera-chat-muted, #6b6b76); font-size: 12px; }
19
+ a { color: inherit; }
20
+ form { display: flex; gap: 8px; padding: 12px; border-top: 1px solid var(--frontera-chat-border, #e3e3e8); }
21
+ textarea { flex: 1; resize: none; font: inherit; padding: 8px 10px; border: 1px solid var(--frontera-chat-border, #e3e3e8); border-radius: 8px; min-height: 20px; max-height: 120px; }
22
+ button { font: inherit; padding: 0 14px; border: 0; border-radius: 8px; background: var(--frontera-chat-accent, #2f5bea); color: var(--frontera-chat-accent-text, #fff); cursor: pointer; }
23
+ button:disabled { opacity: .5; cursor: default; }
24
+ `;
25
+ var Base = typeof HTMLElement === "undefined" ? class {
26
+ } : HTMLElement;
27
+ var FronteraChatElement = class extends Base {
28
+ static observedAttributes = ["chat-title", "placeholder"];
29
+ chat = null;
30
+ log;
31
+ input;
32
+ sendButton;
33
+ header;
34
+ busy = false;
35
+ connectedCallback() {
36
+ if (this.chat) return;
37
+ const root = this.shadowRoot ?? this.attachShadow({ mode: "open" });
38
+ const doc = this.ownerDocument;
39
+ const style = doc.createElement("style");
40
+ style.textContent = STYLE;
41
+ const frame = doc.createElement("div");
42
+ frame.className = "frame";
43
+ this.header = doc.createElement("header");
44
+ this.log = doc.createElement("div");
45
+ this.log.className = "log";
46
+ this.log.setAttribute("role", "log");
47
+ this.log.setAttribute("aria-live", "polite");
48
+ const form = doc.createElement("form");
49
+ this.input = doc.createElement("textarea");
50
+ this.input.rows = 1;
51
+ this.input.setAttribute("aria-label", "Message");
52
+ this.sendButton = doc.createElement("button");
53
+ this.sendButton.type = "submit";
54
+ this.sendButton.textContent = "Send";
55
+ form.append(this.input, this.sendButton);
56
+ frame.append(this.header, this.log, form);
57
+ root.replaceChildren(style, frame);
58
+ this.applyAttributes();
59
+ form.addEventListener("submit", (e) => {
60
+ e.preventDefault();
61
+ void this.submit();
62
+ });
63
+ this.input.addEventListener("keydown", (e) => {
64
+ if (e.key === "Enter" && !e.shiftKey) {
65
+ e.preventDefault();
66
+ void this.submit();
67
+ }
68
+ });
69
+ const apiBaseUrl = this.getAttribute("api-base-url")?.replace(/\/+$/, "");
70
+ const publishableKey = this.getAttribute("publishable-key");
71
+ if (!apiBaseUrl || !publishableKey) {
72
+ this.note("This chat is missing its api-base-url or publishable-key.");
73
+ this.setBusy(true);
74
+ return;
75
+ }
76
+ this.chat = new PublicChat({ apiBaseUrl, publishableKey });
77
+ this.chat.open().then(({ openingMessage }) => {
78
+ if (openingMessage) this.agentBubble().replaceChildren(...this.render(openingMessage));
79
+ }).catch(() => {
80
+ });
81
+ }
82
+ attributeChangedCallback() {
83
+ if (this.header) this.applyAttributes();
84
+ }
85
+ applyAttributes() {
86
+ const title = this.getAttribute("chat-title");
87
+ this.header.textContent = title ?? "";
88
+ this.header.hidden = !title;
89
+ this.input.placeholder = this.getAttribute("placeholder") ?? "Type a message";
90
+ }
91
+ setBusy(busy) {
92
+ this.busy = busy;
93
+ this.sendButton.disabled = busy;
94
+ }
95
+ note(text) {
96
+ const el = this.ownerDocument.createElement("div");
97
+ el.className = "note";
98
+ el.textContent = text;
99
+ this.log.append(el);
100
+ }
101
+ agentBubble() {
102
+ const el = this.ownerDocument.createElement("div");
103
+ el.className = "msg agent";
104
+ this.log.append(el);
105
+ return el;
106
+ }
107
+ /** The reply as paragraphs of text, bold and links, built as nodes: nothing from the reply becomes HTML. */
108
+ render(text) {
109
+ const doc = this.ownerDocument;
110
+ return replyParagraphs(text, { showSources: this.hasAttribute("show-sources") }).map((lines) => {
111
+ const p = doc.createElement("p");
112
+ lines.forEach((pieces, i) => {
113
+ if (i > 0) p.append(doc.createElement("br"));
114
+ for (const piece of pieces) {
115
+ if (piece.kind === "text") p.append(doc.createTextNode(piece.text));
116
+ else if (piece.kind === "bold") {
117
+ const b = doc.createElement("strong");
118
+ b.textContent = piece.text;
119
+ p.append(b);
120
+ } else {
121
+ const a = doc.createElement("a");
122
+ a.href = piece.href;
123
+ a.textContent = piece.text;
124
+ a.target = "_blank";
125
+ a.rel = "noopener noreferrer";
126
+ p.append(a);
127
+ }
128
+ }
129
+ });
130
+ return p;
131
+ });
132
+ }
133
+ async submit() {
134
+ const text = this.input.value.trim();
135
+ if (!text || this.busy || !this.chat) return;
136
+ this.input.value = "";
137
+ const mine = this.ownerDocument.createElement("div");
138
+ mine.className = "msg visitor";
139
+ mine.textContent = text;
140
+ this.log.append(mine);
141
+ this.setBusy(true);
142
+ const typing = this.ownerDocument.createElement("div");
143
+ typing.className = "typing";
144
+ typing.textContent = "Typing\u2026";
145
+ let bubble = null;
146
+ try {
147
+ for await (const event of this.chat.send(text)) {
148
+ if (event.type === "waiting") this.log.append(typing);
149
+ else if (event.type === "text") {
150
+ typing.remove();
151
+ bubble ??= this.agentBubble();
152
+ bubble.replaceChildren(...this.render(event.text));
153
+ } else if (event.type === "error") {
154
+ typing.remove();
155
+ this.note(visitorErrorWords(event.error));
156
+ this.dispatchEvent(new CustomEvent("frontera-chat-error", { detail: { code: event.error.code, message: event.error.message } }));
157
+ }
158
+ this.log.scrollTop = this.log.scrollHeight;
159
+ }
160
+ } finally {
161
+ typing.remove();
162
+ this.setBusy(false);
163
+ this.input.focus();
164
+ }
165
+ }
166
+ };
167
+ function defineFronteraChat(tag = "frontera-chat") {
168
+ if (typeof customElements === "undefined" || customElements.get(tag)) return;
169
+ customElements.define(tag, class extends FronteraChatElement {
170
+ });
171
+ }
172
+
173
+ export { FronteraChatElement, defineFronteraChat };
174
+ //# sourceMappingURL=element.js.map
175
+ //# sourceMappingURL=element.js.map