@nexalink/carecompass-sdk 0.4.0-beta.15 → 0.6.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/dist/telemetry.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { careCompassApiBaseUrlSchema, } from "./schema.js";
1
2
  import { CARECOMPASS_COMPONENT_VERSION, CARECOMPASS_SDK_VERSION, CARECOMPASS_TELEMETRY_CONTRACT_VERSION, } from "./version.js";
2
3
  export const CARECOMPASS_TELEMETRY_EVENTS = {
3
4
  SDK_INITIALIZED: "carecompass_sdk_initialized",
@@ -62,39 +63,71 @@ export const CARECOMPASS_TELEMETRY_ERROR_CODES = {
62
63
  CLI_COMMAND_UNKNOWN: "CLI_COMMAND_UNKNOWN",
63
64
  UNKNOWN_ERROR: "UNKNOWN_ERROR",
64
65
  };
65
- const DEPLOYMENT_ID_RE = /^[A-Za-z0-9][A-Za-z0-9_-]{2,127}$/;
66
- const POSTHOG_PROJECT_TOKEN_RE = /^phc_[A-Za-z0-9_-]{8,}$/;
67
- const POSTHOG_HOSTNAMES = new Set(["us.i.posthog.com", "eu.i.posthog.com"]);
68
- const CARECOMPASS_DEFAULT_POSTHOG_API_HOST = "https://us.i.posthog.com";
69
- const CARECOMPASS_DEFAULT_POSTHOG_PROJECT_TOKEN = "phc_vYl3rpIu6Iz5vCeJUG62BBA3nlPCeF4RSYpCb8FjnOl";
66
+ const CARECOMPASS_DEFAULT_API_BASE_URL = "https://www.nexalink.care";
67
+ const CARECOMPASS_TELEMETRY_PATH = "/api/carecompass/telemetry";
70
68
  const TELEMETRY_CAPTURE_TIMEOUT_MS = 1_000;
69
+ const TELEMETRY_EVENTS = new Set(Object.values(CARECOMPASS_TELEMETRY_EVENTS));
70
+ const TELEMETRY_ERROR_CODES = new Set(Object.values(CARECOMPASS_TELEMETRY_ERROR_CODES));
71
+ const FAILURE_CATEGORIES = new Set([
72
+ "authentication",
73
+ "auth_error",
74
+ "baa_not_signed",
75
+ "configuration",
76
+ "config_error",
77
+ "inference",
78
+ "model_error",
79
+ "network",
80
+ "network_error",
81
+ "permission_error",
82
+ "rate_limit",
83
+ "rate_limit_error",
84
+ "rendering",
85
+ "timeout",
86
+ "unknown",
87
+ ]);
88
+ const AUTH_METHODS = new Set(["api_key", "env_var", "config_file"]);
89
+ const BAA_STATUSES = new Set(["signed", "pending", "not_required"]);
90
+ const COMMAND_NAMES = new Set([
91
+ "auth",
92
+ "completion",
93
+ "config",
94
+ "configure",
95
+ "doctor",
96
+ "embed",
97
+ "health",
98
+ "help",
99
+ "init",
100
+ "login",
101
+ "logout",
102
+ "query",
103
+ "setup",
104
+ "snippet",
105
+ "unknown_command",
106
+ "validate",
107
+ "version",
108
+ ]);
109
+ const EXIT_REASONS = new Set([
110
+ "user_exit",
111
+ "error",
112
+ "timeout",
113
+ "signal",
114
+ ]);
115
+ const AUTH_RESULTS = new Set(["success", "failure"]);
116
+ const KEY_SOURCES = new Set(["env_var", "config_file", "cli_flag"]);
71
117
  const disabledTelemetry = {
72
118
  enabled: false,
73
119
  capture: () => undefined,
74
120
  captureAsync: async () => undefined,
75
121
  };
76
- const approvedApiHost = (value, environment) => {
77
- // Telemetry is restricted to known PostHog ingestion hosts; localhost is only
78
- // accepted in development so tests and local captures can be isolated.
79
- if (typeof value !== "string")
122
+ const approvedRelayBaseUrl = (value, environment) => {
123
+ const parsed = careCompassApiBaseUrlSchema.safeParse(value);
124
+ if (!parsed.success)
80
125
  return null;
81
- try {
82
- const url = new URL(value);
83
- if (url.pathname !== "/" || url.search || url.hash)
84
- return null;
85
- const isLocalhost = url.hostname === "localhost" ||
86
- url.hostname === "127.0.0.1" ||
87
- url.hostname === "[::1]";
88
- if (isLocalhost) {
89
- return environment === "development" ? value.replace(/\/+$/, "") : null;
90
- }
91
- return url.protocol === "https:" && POSTHOG_HOSTNAMES.has(url.hostname)
92
- ? value.replace(/\/+$/, "")
93
- : null;
94
- }
95
- catch {
96
- return null;
97
- }
126
+ const url = new URL(parsed.data);
127
+ const isLocalhost = url.hostname === "localhost" ||
128
+ url.hostname === "127.0.0.1" ||
129
+ url.hostname === "[::1]";
130
+ return isLocalhost && environment !== "development" ? null : parsed.data;
98
131
  };
99
132
  export const careCompassTelemetryDurationBucket = (durationMs) => {
100
133
  if (durationMs < 250)
@@ -107,117 +140,102 @@ export const careCompassTelemetryDurationBucket = (durationMs) => {
107
140
  return "5_to_29_999_ms";
108
141
  return "30_s_or_more";
109
142
  };
110
- const integerMs = (value) => Math.max(0, Math.round(value));
111
- const integerCount = (value) => Number.isFinite(value) ? Math.max(0, Math.trunc(value)) : 0;
143
+ const integerMs = (value) => typeof value === "number" && Number.isFinite(value)
144
+ ? Math.max(0, Math.round(value))
145
+ : undefined;
146
+ const integerCount = (value) => typeof value === "number" && Number.isFinite(value)
147
+ ? Math.max(0, Math.trunc(value))
148
+ : undefined;
149
+ const allowedValue = (value, values) => typeof value === "string" && values.has(value) ? value : undefined;
150
+ const allowedEnvironment = (value) => value === "development" || value === "staging" || value === "production"
151
+ ? value
152
+ : "unknown";
112
153
  function createTelemetry(context, options = {}) {
113
- const projectToken = CARECOMPASS_DEFAULT_POSTHOG_PROJECT_TOKEN;
114
- // Invalid identifiers disable telemetry instead of producing malformed events
115
- // or sending data under an ambiguous deployment identity.
116
- if (!DEPLOYMENT_ID_RE.test(context.deploymentId) ||
117
- !DEPLOYMENT_ID_RE.test(context.distinctId) ||
118
- !POSTHOG_PROJECT_TOKEN_RE.test(projectToken)) {
119
- return disabledTelemetry;
120
- }
121
154
  const fetcher = options.fetcher ?? fetch;
122
155
  const now = options.now ?? (() => new Date());
123
- const apiHost = approvedApiHost(CARECOMPASS_DEFAULT_POSTHOG_API_HOST, context.environment);
124
- if (!apiHost)
156
+ const relayBaseUrl = approvedRelayBaseUrl(context.apiBaseUrl, context.environment);
157
+ if (!relayBaseUrl)
125
158
  return disabledTelemetry;
126
159
  const captureAsync = async (event, captureOptions = {}) => {
160
+ if (!TELEMETRY_EVENTS.has(event))
161
+ return;
127
162
  const eventTimestamp = now().toISOString();
163
+ const durationMs = integerMs(captureOptions.durationMs);
164
+ const failureCategory = captureOptions.failureCategory === null
165
+ ? null
166
+ : allowedValue(captureOptions.failureCategory, FAILURE_CATEGORIES);
167
+ const errorCode = allowedValue(captureOptions.errorCode, TELEMETRY_ERROR_CODES);
168
+ const initDurationMs = integerMs(captureOptions.initDurationMs);
169
+ const authMethod = allowedValue(captureOptions.authMethod, AUTH_METHODS);
170
+ const baaStatus = allowedValue(captureOptions.baaStatus, BAA_STATUSES);
171
+ const commandName = allowedValue(captureOptions.commandName, COMMAND_NAMES);
172
+ const commandArgsCount = integerCount(captureOptions.commandArgsCount);
173
+ const commandDurationMs = integerMs(captureOptions.commandDurationMs);
174
+ const tokensUsed = captureOptions.tokensUsed === null
175
+ ? null
176
+ : integerCount(captureOptions.tokensUsed);
177
+ const sessionDurationMs = integerMs(captureOptions.sessionDurationMs);
178
+ const commandsAttempted = integerCount(captureOptions.commandsAttempted);
179
+ const commandsSucceeded = integerCount(captureOptions.commandsSucceeded);
180
+ const commandsFailed = integerCount(captureOptions.commandsFailed);
181
+ const exitReason = allowedValue(captureOptions.exitReason, EXIT_REASONS);
182
+ const authResult = allowedValue(captureOptions.authResult, AUTH_RESULTS);
183
+ const keySource = allowedValue(captureOptions.keySource, KEY_SOURCES);
128
184
  // Properties are allowlisted here by design. Do not add prompts, responses,
129
- // user context, credentials, or raw exception messages to this payload.
185
+ // user context, credentials, raw exception messages, or stable identifiers.
130
186
  const properties = {
131
187
  $process_person_profile: false,
132
188
  contract_version: CARECOMPASS_TELEMETRY_CONTRACT_VERSION,
133
189
  sdk_version: CARECOMPASS_SDK_VERSION,
134
190
  component_version: CARECOMPASS_COMPONENT_VERSION,
135
191
  platform: context.platform,
136
- environment: context.environment,
137
- payer_id: context.payerId,
138
- deployment_id: context.deploymentId,
192
+ environment: allowedEnvironment(context.environment),
139
193
  event_timestamp: eventTimestamp,
140
- ...(context.cliSessionId ? { cli_session_id: context.cliSessionId } : {}),
141
- ...(captureOptions.durationMs === undefined
194
+ ...(durationMs === undefined
142
195
  ? {}
143
196
  : {
144
- duration_bucket: careCompassTelemetryDurationBucket(captureOptions.durationMs),
197
+ duration_bucket: careCompassTelemetryDurationBucket(durationMs),
145
198
  }),
146
- ...(captureOptions.failureCategory === undefined
199
+ ...(failureCategory === undefined
147
200
  ? {}
148
- : { failure_category: captureOptions.failureCategory }),
149
- ...(captureOptions.errorCode
150
- ? { error_code: captureOptions.errorCode }
151
- : {}),
152
- ...(captureOptions.initDurationMs === undefined
201
+ : { failure_category: failureCategory }),
202
+ ...(errorCode ? { error_code: errorCode } : {}),
203
+ ...(initDurationMs === undefined
153
204
  ? {}
154
- : { init_duration_ms: integerMs(captureOptions.initDurationMs) }),
155
- ...(captureOptions.authMethod
156
- ? { auth_method: captureOptions.authMethod }
157
- : {}),
158
- ...(captureOptions.baaStatus
159
- ? { baa_status: captureOptions.baaStatus }
160
- : {}),
161
- ...(captureOptions.commandName
162
- ? { command_name: captureOptions.commandName }
163
- : {}),
164
- ...(captureOptions.commandArgsCount === undefined
205
+ : { init_duration_ms: initDurationMs }),
206
+ ...(authMethod ? { auth_method: authMethod } : {}),
207
+ ...(baaStatus ? { baa_status: baaStatus } : {}),
208
+ ...(commandName ? { command_name: commandName } : {}),
209
+ ...(commandArgsCount === undefined
165
210
  ? {}
166
- : {
167
- command_args_count: integerCount(captureOptions.commandArgsCount),
168
- }),
169
- ...(captureOptions.commandDurationMs === undefined
211
+ : { command_args_count: commandArgsCount }),
212
+ ...(commandDurationMs === undefined
170
213
  ? {}
171
- : {
172
- command_duration_ms: integerMs(captureOptions.commandDurationMs),
173
- }),
174
- ...(captureOptions.tokensUsed === undefined
214
+ : { command_duration_ms: commandDurationMs }),
215
+ ...(tokensUsed === undefined ? {} : { tokens_used: tokensUsed }),
216
+ ...(sessionDurationMs === undefined
175
217
  ? {}
176
- : {
177
- tokens_used: captureOptions.tokensUsed === null
178
- ? null
179
- : integerCount(captureOptions.tokensUsed),
180
- }),
181
- ...(captureOptions.modelId === undefined
218
+ : { session_duration_ms: sessionDurationMs }),
219
+ ...(commandsAttempted === undefined
182
220
  ? {}
183
- : { model_id: captureOptions.modelId }),
184
- ...(captureOptions.sessionDurationMs === undefined
221
+ : { commands_attempted: commandsAttempted }),
222
+ ...(commandsSucceeded === undefined
185
223
  ? {}
186
- : {
187
- session_duration_ms: integerMs(captureOptions.sessionDurationMs),
188
- }),
189
- ...(captureOptions.commandsAttempted === undefined
190
- ? {}
191
- : {
192
- commands_attempted: integerCount(captureOptions.commandsAttempted),
193
- }),
194
- ...(captureOptions.commandsSucceeded === undefined
195
- ? {}
196
- : {
197
- commands_succeeded: integerCount(captureOptions.commandsSucceeded),
198
- }),
199
- ...(captureOptions.commandsFailed === undefined
224
+ : { commands_succeeded: commandsSucceeded }),
225
+ ...(commandsFailed === undefined
200
226
  ? {}
201
- : { commands_failed: integerCount(captureOptions.commandsFailed) }),
202
- ...(captureOptions.exitReason
203
- ? { exit_reason: captureOptions.exitReason }
204
- : {}),
205
- ...(captureOptions.authResult
206
- ? { auth_result: captureOptions.authResult }
207
- : {}),
208
- ...(captureOptions.keySource
209
- ? { key_source: captureOptions.keySource }
210
- : {}),
227
+ : { commands_failed: commandsFailed }),
228
+ ...(exitReason ? { exit_reason: exitReason } : {}),
229
+ ...(authResult ? { auth_result: authResult } : {}),
230
+ ...(keySource ? { key_source: keySource } : {}),
211
231
  };
212
232
  const controller = new AbortController();
213
233
  const timeout = setTimeout(() => controller.abort(), TELEMETRY_CAPTURE_TIMEOUT_MS);
214
234
  try {
215
- await fetcher(`${apiHost}/i/v0/e/`, {
235
+ await fetcher(`${relayBaseUrl}${CARECOMPASS_TELEMETRY_PATH}`, {
216
236
  method: "POST",
217
237
  headers: { "content-type": "application/json" },
218
238
  body: JSON.stringify({
219
- api_key: projectToken,
220
- distinct_id: context.distinctId,
221
239
  event,
222
240
  properties,
223
241
  timestamp: eventTimestamp,
@@ -239,24 +257,19 @@ function createTelemetry(context, options = {}) {
239
257
  };
240
258
  }
241
259
  export function createCareCompassTelemetry(config, options = {}) {
242
- const deploymentId = `${config.payerId}-${config.environment}-web`;
243
260
  return createTelemetry({
244
- deploymentId,
245
- distinctId: deploymentId,
261
+ apiBaseUrl: config.apiBaseUrl,
246
262
  environment: config.environment,
247
- payerId: config.payerId,
248
263
  platform: "web",
249
264
  }, options);
250
265
  }
251
266
  export function createCareCompassCliTelemetry(sessionId, context = {}, options = {}) {
267
+ // Keep the public signature stable, but session IDs must never reach telemetry.
268
+ void sessionId;
252
269
  const environment = context.environment ?? "unknown";
253
- const payerId = context.payerId ?? "anonymous";
254
270
  return createTelemetry({
255
- cliSessionId: sessionId,
256
- deploymentId: `${payerId}-${environment}-cli`,
257
- distinctId: sessionId,
271
+ apiBaseUrl: context.apiBaseUrl ?? CARECOMPASS_DEFAULT_API_BASE_URL,
258
272
  environment,
259
- payerId,
260
273
  platform: "cli",
261
274
  }, options);
262
275
  }
package/dist/ui.d.ts CHANGED
@@ -1,3 +1,3 @@
1
- export { CareCompassChat, CareCompassEmbed } from "./component.js";
2
- export type { CareCompassChatProps } from "./component.js";
1
+ export { CareCompassChat, CareCompassEmbed, useCareCompass, } from "./component.js";
2
+ export type { CareCompassChatProps, CareCompassReactController, UseCareCompassOptions, } from "./component.js";
3
3
  export { mountCareCompass } from "./mount.js";
package/dist/ui.js CHANGED
@@ -1,2 +1,2 @@
1
- export { CareCompassChat, CareCompassEmbed } from "./component.js";
1
+ export { CareCompassChat, CareCompassEmbed, useCareCompass, } from "./component.js";
2
2
  export { mountCareCompass } from "./mount.js";
package/dist/version.d.ts CHANGED
@@ -1,3 +1,3 @@
1
1
  export declare const CARECOMPASS_SDK_VERSION: string;
2
2
  export declare const CARECOMPASS_COMPONENT_VERSION = "1.0.0";
3
- export declare const CARECOMPASS_TELEMETRY_CONTRACT_VERSION = "1.0.0";
3
+ export declare const CARECOMPASS_TELEMETRY_CONTRACT_VERSION = "1.1.0";
package/dist/version.js CHANGED
@@ -1,3 +1,3 @@
1
- export const CARECOMPASS_SDK_VERSION = "0.4.0-beta.15"; // x-release-please-version
1
+ export const CARECOMPASS_SDK_VERSION = "0.6.0"; // x-release-please-version
2
2
  export const CARECOMPASS_COMPONENT_VERSION = "1.0.0";
3
- export const CARECOMPASS_TELEMETRY_CONTRACT_VERSION = "1.0.0";
3
+ export const CARECOMPASS_TELEMETRY_CONTRACT_VERSION = "1.1.0";
@@ -0,0 +1,13 @@
1
+ import type { CareCompassRuntimeTokenProvider } from "./client.js";
2
+ import { type CareCompassConfig } from "./schema.js";
3
+ export declare const CARECOMPASS_ELEMENT_TAG = "carecompass-chat";
4
+ export type CareCompassWebComponentElement = HTMLElement & {
5
+ config: CareCompassConfig | undefined;
6
+ runtimeTokenProvider: CareCompassRuntimeTokenProvider | undefined;
7
+ };
8
+ /**
9
+ * Register a framework-neutral embed backed directly by the headless core.
10
+ * Deferring the HTMLElement subclass creation keeps this entry point safe to
11
+ * import during server rendering.
12
+ */
13
+ export declare function defineCareCompassElement(tagName?: string): CustomElementConstructor | undefined;
@@ -0,0 +1,236 @@
1
+ import { createCareCompassCore, } from "./core.js";
2
+ import { validateCareCompassConfig } from "./schema.js";
3
+ export const CARECOMPASS_ELEMENT_TAG = "carecompass-chat";
4
+ /**
5
+ * Register a framework-neutral embed backed directly by the headless core.
6
+ * Deferring the HTMLElement subclass creation keeps this entry point safe to
7
+ * import during server rendering.
8
+ */
9
+ export function defineCareCompassElement(tagName = CARECOMPASS_ELEMENT_TAG) {
10
+ if (typeof customElements === "undefined" ||
11
+ typeof HTMLElement === "undefined") {
12
+ return undefined;
13
+ }
14
+ const existing = customElements.get(tagName);
15
+ if (existing)
16
+ return existing;
17
+ class CareCompassElement extends HTMLElement {
18
+ constructor() {
19
+ super(...arguments);
20
+ this.draft = "";
21
+ this.hasPropertyConfig = false;
22
+ }
23
+ static get observedAttributes() {
24
+ return ["config"];
25
+ }
26
+ get config() {
27
+ return this.configuredValue;
28
+ }
29
+ set config(value) {
30
+ this.hasPropertyConfig = true;
31
+ this.configuredValue = value;
32
+ this.remount();
33
+ }
34
+ get runtimeTokenProvider() {
35
+ return this.runtimeTokenProviderValue;
36
+ }
37
+ set runtimeTokenProvider(value) {
38
+ this.runtimeTokenProviderValue = value;
39
+ this.remount();
40
+ }
41
+ connectedCallback() {
42
+ this.remount();
43
+ }
44
+ disconnectedCallback() {
45
+ this.unmount();
46
+ }
47
+ attributeChangedCallback(name, _oldValue, value) {
48
+ if (name !== "config" || this.hasPropertyConfig)
49
+ return;
50
+ if (value === null) {
51
+ this.configuredValue = undefined;
52
+ this.remount();
53
+ return;
54
+ }
55
+ try {
56
+ this.configuredValue = JSON.parse(value);
57
+ this.remount();
58
+ }
59
+ catch {
60
+ this.reportConfigurationError();
61
+ }
62
+ }
63
+ unmount() {
64
+ this.unsubscribe?.();
65
+ this.unsubscribe = undefined;
66
+ this.core?.dispose();
67
+ this.core = undefined;
68
+ this.replaceChildren();
69
+ }
70
+ remount() {
71
+ this.unmount();
72
+ if (!this.isConnected || !this.configuredValue)
73
+ return;
74
+ const validation = validateCareCompassConfig(this.configuredValue);
75
+ if (!validation.ok) {
76
+ this.reportConfigurationError();
77
+ return;
78
+ }
79
+ this.core = createCareCompassCore(validation.config, {
80
+ onAuditEvent: (event) => this.dispatchMetadataEvent("carecompass-audit", event),
81
+ onSafetyEvent: (event) => this.dispatchMetadataEvent("carecompass-safety", event),
82
+ runtimeTokenProvider: this.runtimeTokenProviderValue,
83
+ });
84
+ this.unsubscribe = this.core.subscribe((snapshot) => this.renderSnapshot(validation.config, snapshot));
85
+ this.renderSnapshot(validation.config, this.core.getSnapshot());
86
+ }
87
+ dispatchMetadataEvent(name, event) {
88
+ // Core audit and safety records intentionally contain metadata and PHI
89
+ // summaries only; never interpolate message text into DOM events.
90
+ this.dispatchEvent(new CustomEvent(name, {
91
+ bubbles: true,
92
+ detail: event,
93
+ }));
94
+ }
95
+ renderSnapshot(config, snapshot) {
96
+ const section = document.createElement("section");
97
+ section.setAttribute("aria-label", config.labels.title);
98
+ Object.assign(section.style, {
99
+ background: config.theme.backgroundColor,
100
+ border: `1px solid ${config.theme.textColor}22`,
101
+ borderRadius: config.theme.borderRadius,
102
+ color: config.theme.textColor,
103
+ display: "flex",
104
+ flexDirection: "column",
105
+ fontFamily: config.theme.fontFamily,
106
+ maxWidth: config.placement.mode === "sidebar" ? "420px" : "720px",
107
+ minHeight: "420px",
108
+ overflow: "hidden",
109
+ });
110
+ const header = document.createElement("header");
111
+ Object.assign(header.style, {
112
+ borderBottom: `1px solid ${config.theme.textColor}22`,
113
+ padding: "16px",
114
+ });
115
+ const title = document.createElement("strong");
116
+ title.textContent = config.labels.title;
117
+ const brand = document.createElement("div");
118
+ brand.textContent = config.branding.displayName;
119
+ brand.style.fontSize = "12px";
120
+ brand.style.opacity = "0.72";
121
+ header.append(title, brand);
122
+ const messages = document.createElement("div");
123
+ messages.setAttribute("aria-live", "polite");
124
+ Object.assign(messages.style, {
125
+ display: "flex",
126
+ flex: "1",
127
+ flexDirection: "column",
128
+ gap: "12px",
129
+ overflowY: "auto",
130
+ padding: "16px",
131
+ });
132
+ for (const message of snapshot.messages) {
133
+ const bubble = document.createElement("div");
134
+ bubble.textContent = message.text;
135
+ Object.assign(bubble.style, {
136
+ alignSelf: message.role === "user" ? "flex-end" : "flex-start",
137
+ background: message.role === "user"
138
+ ? config.theme.primaryColor
139
+ : `${config.theme.textColor}12`,
140
+ borderRadius: "12px",
141
+ color: message.role === "user" ? "#ffffff" : "inherit",
142
+ maxWidth: "82%",
143
+ padding: "10px 12px",
144
+ whiteSpace: "pre-wrap",
145
+ });
146
+ messages.append(bubble);
147
+ }
148
+ if (snapshot.loading) {
149
+ const thinking = document.createElement("div");
150
+ thinking.textContent = "Thinking...";
151
+ thinking.style.fontSize = "13px";
152
+ thinking.style.opacity = "0.72";
153
+ messages.append(thinking);
154
+ }
155
+ const form = document.createElement("form");
156
+ Object.assign(form.style, {
157
+ borderTop: `1px solid ${config.theme.textColor}22`,
158
+ display: "flex",
159
+ flexDirection: "column",
160
+ gap: "8px",
161
+ padding: "12px",
162
+ });
163
+ form.addEventListener("submit", (event) => {
164
+ event.preventDefault();
165
+ void this.sendMessage(snapshot);
166
+ });
167
+ const controls = document.createElement("div");
168
+ controls.style.display = "flex";
169
+ controls.style.gap = "8px";
170
+ const input = document.createElement("input");
171
+ input.setAttribute("aria-label", config.labels.placeholder);
172
+ input.disabled = snapshot.loading;
173
+ input.placeholder = config.labels.placeholder;
174
+ input.value = this.draft;
175
+ input.style.flex = "1";
176
+ input.style.minWidth = "0";
177
+ input.style.padding = "10px 12px";
178
+ input.addEventListener("input", () => {
179
+ this.draft = input.value;
180
+ send.disabled = snapshot.loading || !this.draft.trim();
181
+ });
182
+ const send = document.createElement("button");
183
+ send.disabled = snapshot.loading || !this.draft.trim();
184
+ send.textContent = config.labels.sendButton;
185
+ send.type = "submit";
186
+ Object.assign(send.style, {
187
+ background: config.theme.primaryColor,
188
+ border: "0",
189
+ borderRadius: "8px",
190
+ color: "#ffffff",
191
+ fontWeight: "700",
192
+ padding: "0 14px",
193
+ });
194
+ controls.append(input, send);
195
+ form.append(controls);
196
+ if (snapshot.error) {
197
+ const error = document.createElement("div");
198
+ error.setAttribute("role", "alert");
199
+ error.textContent = snapshot.error.message;
200
+ error.style.color = "#b91c1c";
201
+ error.style.fontSize = "12px";
202
+ form.append(error);
203
+ }
204
+ const disclaimer = document.createElement("div");
205
+ disclaimer.textContent = config.labels.disclaimer;
206
+ disclaimer.style.fontSize = "11px";
207
+ disclaimer.style.opacity = "0.7";
208
+ form.append(disclaimer);
209
+ section.append(header, messages, form);
210
+ this.replaceChildren(section);
211
+ }
212
+ async sendMessage(snapshot) {
213
+ const prompt = this.draft.trim();
214
+ if (!prompt || snapshot.loading || !this.core)
215
+ return;
216
+ this.draft = "";
217
+ try {
218
+ this.core.recordWidgetAction();
219
+ await this.core.sendMessage(prompt);
220
+ }
221
+ catch {
222
+ // The snapshot already carries the safe, user-visible error state.
223
+ }
224
+ }
225
+ reportConfigurationError() {
226
+ // Do not include raw config or validation detail in DOM events; a config
227
+ // can contain user-controlled branding and labels.
228
+ this.dispatchEvent(new CustomEvent("carecompass-error", {
229
+ bubbles: true,
230
+ detail: { code: "configuration_invalid" },
231
+ }));
232
+ }
233
+ }
234
+ customElements.define(tagName, CareCompassElement);
235
+ return CareCompassElement;
236
+ }