@richardmcquiston01/chat-and-react-core 0.5.0 → 0.7.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/index.d.ts CHANGED
@@ -1,3 +1,10 @@
1
+ import { StorageAdapter } from '@richardmcquiston01/storage-adapters';
2
+ export { StorageAdapter, StoreErrorCode, StoreResult, StoredValue } from '@richardmcquiston01/storage-adapters';
3
+ export { LocalStorageAdapterOptions, SessionStorageAdapterOptions, StorageChangeEvent, StorageEventTarget, WebStorageAdapterOptions, WebStorageLike, WebStorageName, localStorageAdapter, sessionStorageAdapter } from '@richardmcquiston01/storage-adapters/browser';
4
+ export { IndexedDbAdapterOptions, indexedDbAdapter } from '@richardmcquiston01/storage-adapters/indexeddb';
5
+ import { ErrorLogEntry, ErrorLogAdapter as ErrorLogAdapter$1, ConsoleErrorAdapter as ConsoleErrorAdapter$1, ConsoleErrorOptions } from '@richardmcquiston01/error-log-adapter';
6
+ export { ErrorLogEntry, ErrorLogResult } from '@richardmcquiston01/error-log-adapter';
7
+
1
8
  type AnswerMap = Record<string, string | string[]>;
2
9
  type ChatInputType = 'text' | 'textarea' | 'dropdown' | 'radio' | 'checkbox';
3
10
  type JsonLogicRule = Record<string, unknown>;
@@ -11,6 +18,16 @@ interface ChatQuestion {
11
18
  title: string;
12
19
  'x-chat-placeholder'?: string;
13
20
  'x-chat-condition'?: JsonLogicRule | ChatConditionRef;
21
+ /**
22
+ * Shows a "thinking" bubble before this question appears, so the conversation
23
+ * feels less abrupt. A number is the delay in milliseconds (0 turns it off for
24
+ * this question). An object can also set the text shown instead of the dots.
25
+ * Overrides the `thinkingDelay` and `thinkingText` options.
26
+ */
27
+ 'x-chat-thinking'?: number | {
28
+ delay?: number;
29
+ text?: string;
30
+ };
14
31
  enum?: string[];
15
32
  items?: {
16
33
  enum: string[];
@@ -33,13 +50,31 @@ interface ChatPage {
33
50
  questions: ChatQuestion[];
34
51
  branching: BranchingRule[];
35
52
  }
53
+ /** How a bubble leaves the screen when it is hidden: fade out, shrink away ("pop"), or vanish at once. */
54
+ type HideAnimation = 'fade' | 'pop' | 'none';
36
55
  interface ChatOptions {
37
56
  userResumable: boolean;
38
57
  localStorage: boolean;
39
58
  autoComplete: boolean;
59
+ /** Show an Edit icon on answered questions so users can change their answers. */
60
+ allowEdit: boolean;
61
+ /** Milliseconds to wait before hiding an answered question bubble. 0 keeps them visible. */
62
+ autoHideQuestions: number;
63
+ /** Milliseconds to wait before hiding an answer bubble once its question is answered. 0 keeps them visible. */
64
+ autoHideAnswers: number;
65
+ /** How hidden bubbles leave the screen. */
66
+ hideAnimation: HideAnimation;
67
+ /** Milliseconds per character when question text is "typed" onto the screen. 0 shows it at once. */
68
+ typingSpeed: number;
69
+ /** Milliseconds to show a "thinking" bubble before each question. 0 turns it off. */
70
+ thinkingDelay: number;
71
+ /** Text for the thinking bubble. Empty shows animated dots. */
72
+ thinkingText: string;
73
+ /** Show an icon on the Continue button. */
74
+ showContinueIcon: boolean;
40
75
  }
41
76
  interface ChatSchema {
42
- '$schema'?: string;
77
+ $schema?: string;
43
78
  'x-chat-version': '1';
44
79
  'x-chat-options'?: Partial<ChatOptions>;
45
80
  'x-chat-pages': ChatPage[];
@@ -51,7 +86,23 @@ interface PageSubmitEvent {
51
86
  pageId: string;
52
87
  visitIndex: number;
53
88
  answers: AnswerMap;
89
+ /** Present and true when the user re-submitted a page they had already answered. */
90
+ edited?: boolean;
91
+ }
92
+ /** One page the user has submitted, in the order they visited it. */
93
+ interface RouteEntry {
94
+ pageId: string;
95
+ answers: AnswerMap;
54
96
  }
97
+ /**
98
+ * What happened to the form after `submitPage`:
99
+ * - `advanced`: a normal submit; the form moved to the next page or completed.
100
+ * - `edit-resumed`: an earlier page was edited and the route is unchanged, so the
101
+ * form is back on the page the user was on.
102
+ * - `edit-rerouted`: an earlier page was edited and its branching now leads
103
+ * somewhere else. Later pages were dropped and the form moved to the new page.
104
+ */
105
+ type SubmitOutcome = 'advanced' | 'edit-resumed' | 'edit-rerouted';
55
106
  interface FormCompleteEvent {
56
107
  type: 'form:complete';
57
108
  sessionId: string;
@@ -59,49 +110,42 @@ interface FormCompleteEvent {
59
110
  }
60
111
  type ChatEvent = PageSubmitEvent | FormCompleteEvent;
61
112
  type ChatErrorCode = 'SCHEMA_INVALID' | 'RULE_EVAL_FAILED' | 'ADAPTER_ERROR' | 'STORAGE_UNAVAILABLE';
62
- interface ChatError {
63
- code: ChatErrorCode;
64
- message: string;
65
- cause?: unknown;
66
- }
113
+ type ChatError = ErrorLogEntry<ChatErrorCode>;
67
114
  interface InputAdapter {
68
115
  transform(raw: unknown): unknown;
69
116
  }
70
117
  interface OutputAdapter {
71
118
  emit(event: ChatEvent): void;
72
119
  }
73
- interface ErrorLogAdapter {
74
- log(error: ChatError): void;
75
- }
120
+ /**
121
+ * Routes `ChatError`s to a sink. `log` returns a result instead of throwing, so the
122
+ * caller can check `ok`. See `@richardmcquiston01/error-log-adapter`.
123
+ */
124
+ type ErrorLogAdapter = ErrorLogAdapter$1<ChatErrorCode>;
125
+
76
126
  interface ChatControllerConfig {
77
127
  schema: unknown;
78
128
  inputAdapter?: InputAdapter;
79
129
  outputAdapter?: OutputAdapter;
80
130
  errorAdapter?: ErrorLogAdapter;
131
+ /**
132
+ * Option values that win over the schema's `x-chat-options`, for example ones
133
+ * read from element attributes.
134
+ */
135
+ options?: Partial<ChatOptions>;
81
136
  }
82
137
 
83
- /**
84
- * Parses and validates an unknown input as a {@link ChatSchema}.
85
- *
86
- * @param raw - The raw input to validate.
87
- * @returns The typed {@link ChatSchema} if valid.
88
- * @throws `{ code: 'SCHEMA_INVALID', message: string }` on any validation failure.
89
- */
90
- declare function parseSchema(raw: unknown): ChatSchema;
138
+ declare class BrowserEventAdapter implements OutputAdapter {
139
+ emit(event: ChatEvent): void;
140
+ }
91
141
 
92
- /**
93
- * Evaluates JSON Logic rules against provided answers.
94
- * Wraps json-logic-js to provide type-safe rule evaluation.
95
- */
96
- declare class RuleEngine {
97
- /**
98
- * Evaluates a JSON Logic rule against an answer map.
99
- * @param rule The JSON Logic rule to evaluate
100
- * @param answers The answer map to evaluate the rule against
101
- * @returns The boolean result of the rule evaluation
102
- * @throws {{ code: 'RULE_EVAL_FAILED', message: string, cause: unknown }} if rule evaluation fails
103
- */
104
- evaluate(rule: JsonLogicRule, answers: AnswerMap): boolean;
142
+ /** Writes errors to `console.error`, prefixed with `[chat-and-react]` unless overridden. */
143
+ declare class ConsoleErrorAdapter extends ConsoleErrorAdapter$1<ChatErrorCode> {
144
+ constructor(options?: ConsoleErrorOptions);
145
+ }
146
+
147
+ declare class IdentityInputAdapter implements InputAdapter {
148
+ transform(raw: unknown): unknown;
105
149
  }
106
150
 
107
151
  interface StorageBackend {
@@ -115,7 +159,22 @@ interface SessionProgress {
115
159
  currentPageId: string;
116
160
  answers: AnswerMap;
117
161
  }
118
- declare class SessionManager {
162
+ /**
163
+ * What `ChatController` needs from session storage. `SessionManager` keeps it in a
164
+ * synchronous Web Storage object; `AdapterSessionManager` keeps it in a `StorageAdapter`.
165
+ */
166
+ interface SessionStore {
167
+ readonly isStorageAvailable: boolean;
168
+ getSessionId(): string;
169
+ saveProgress(currentPageId: string, answers: AnswerMap): void;
170
+ loadProgress(): SessionProgress | null;
171
+ saveInput(questionId: string, value: string | string[]): void;
172
+ loadInput(questionId: string): string | string[] | null;
173
+ clearAll(): void;
174
+ /** Stops listening for changes made elsewhere. Called when the controller is destroyed. */
175
+ dispose?(): void;
176
+ }
177
+ declare class SessionManager implements SessionStore {
119
178
  private readonly storage;
120
179
  private readonly sessionId;
121
180
  private readonly available;
@@ -146,12 +205,18 @@ declare class ChatController {
146
205
  private currentPage;
147
206
  private allAnswers;
148
207
  private visitIndex;
208
+ /** Pages submitted in this session, in visit order. Used for editing earlier answers. */
209
+ private route;
210
+ /** Index in `route` of the page being edited, or null when not editing. */
211
+ private editIndex;
212
+ /** The page the user was on before they started editing an earlier one. */
213
+ private resumePageId;
149
214
  private readonly listeners;
150
215
  /**
151
216
  * @param config - Controller configuration including schema and optional adapters.
152
217
  * @param sessionManager - Optional session manager for test injection.
153
218
  */
154
- constructor(config: ChatControllerConfig, sessionManager?: SessionManager);
219
+ constructor(config: ChatControllerConfig, sessionManager?: SessionStore);
155
220
  /**
156
221
  * Initialises the controller: parses the schema, resolves options,
157
222
  * restores any saved session, and positions to the first page.
@@ -170,6 +235,24 @@ declare class ChatController {
170
235
  * Returns the resolved options for the current session (schema options merged with defaults).
171
236
  */
172
237
  getOptions(): ChatOptions;
238
+ /**
239
+ * Returns the pages the user has submitted in this session, in visit order.
240
+ * A session restored from storage starts with an empty route.
241
+ */
242
+ getRoute(): RouteEntry[];
243
+ /**
244
+ * Returns every answer given so far, for pre-filling inputs.
245
+ */
246
+ getAnswers(): AnswerMap;
247
+ /**
248
+ * Re-opens a page the user already submitted so its answers can be changed.
249
+ * Call `submitPage` afterwards to save the new answers. Not available once the
250
+ * form is complete.
251
+ *
252
+ * @param routeIndex - Position of the page in `getRoute()`.
253
+ * @returns true when the page was re-opened.
254
+ */
255
+ reopenPage(routeIndex: number): boolean;
173
256
  /**
174
257
  * Submits the answers for the current page and advances the session.
175
258
  * - Accumulates answers into `allAnswers`.
@@ -177,9 +260,15 @@ declare class ChatController {
177
260
  * - Resolves branching to find the next page.
178
261
  * - Emits `form:complete` and clears session when `**FORM_COMPLETED**` is reached.
179
262
  *
263
+ * When the page was re-opened with `reopenPage`, the new answers replace the
264
+ * old ones and `page:submit` is emitted with `edited: true`. If the page's
265
+ * branching still leads to the same next page, the form returns to the page the
266
+ * user was on. If it leads somewhere else, the pages that followed it are
267
+ * dropped, along with their answers, and the form continues from the new page.
268
+ *
180
269
  * @param pageAnswers - Answers collected for the current page.
181
270
  */
182
- submitPage(pageAnswers: AnswerMap): void;
271
+ submitPage(pageAnswers: AnswerMap): SubmitOutcome;
183
272
  /**
184
273
  * Registers an event listener for the given event type.
185
274
  *
@@ -203,6 +292,12 @@ declare class ChatController {
203
292
  * Returns true when no condition is present. On evaluation failure, logs and returns false.
204
293
  */
205
294
  private isQuestionVisible;
295
+ /**
296
+ * The answers question conditions are checked against. While an earlier page is
297
+ * being edited, that page's own answers are left out, so its questions are
298
+ * shown exactly as they were the first time the page was asked.
299
+ */
300
+ private answersForRules;
206
301
  /**
207
302
  * Walks the branching rules top-to-bottom. Returns the first match's destination_id.
208
303
  * Falls back to `**FORM_COMPLETED**` if nothing matches (should not happen with valid schemas).
@@ -221,18 +316,91 @@ declare class ChatController {
221
316
  * Used for unrecoverable errors (schema invalid, adapter failure on start).
222
317
  */
223
318
  private handleFatal;
319
+ /**
320
+ * Sends the error to the error adapter without waiting for it. If the adapter
321
+ * reports a failure, rejects, or throws, that is written to `console.error` so
322
+ * the original error is never lost and the form keeps running.
323
+ */
324
+ private logError;
224
325
  }
225
326
 
226
- declare class IdentityInputAdapter implements InputAdapter {
227
- transform(raw: unknown): unknown;
327
+ /**
328
+ * Evaluates JSON Logic rules against provided answers.
329
+ * Wraps json-logic-js to provide type-safe rule evaluation.
330
+ */
331
+ declare class RuleEngine {
332
+ /**
333
+ * Evaluates a JSON Logic rule against an answer map.
334
+ * @param rule The JSON Logic rule to evaluate
335
+ * @param answers The answer map to evaluate the rule against
336
+ * @returns The boolean result of the rule evaluation
337
+ * @throws {{ code: 'RULE_EVAL_FAILED', message: string, cause: unknown }} if rule evaluation fails
338
+ */
339
+ evaluate(rule: JsonLogicRule, answers: AnswerMap): boolean;
228
340
  }
229
341
 
230
- declare class BrowserEventAdapter implements OutputAdapter {
231
- emit(event: ChatEvent): void;
232
- }
342
+ /**
343
+ * Parses and validates an unknown input as a {@link ChatSchema}.
344
+ *
345
+ * @param raw - The raw input to validate.
346
+ * @returns The typed {@link ChatSchema} if valid.
347
+ * @throws `{ code: 'SCHEMA_INVALID', message: string }` on any validation failure.
348
+ */
349
+ declare function parseSchema(raw: unknown): ChatSchema;
233
350
 
234
- declare class ConsoleErrorAdapter implements ErrorLogAdapter {
235
- log(error: ChatError): void;
351
+ type InputValue = string | string[];
352
+ interface AdapterSessionManagerOptions {
353
+ /** Called when the adapter fails. The session keeps working from memory. */
354
+ onError?: (error: ChatError) => void;
355
+ /**
356
+ * Called when saved progress changes because of a write made elsewhere, such as
357
+ * another tab. `null` means the session was cleared there.
358
+ */
359
+ onExternalChange?: (progress: SessionProgress | null) => void;
360
+ }
361
+ /**
362
+ * A session store backed by a `StorageAdapter`. `ChatController` is synchronous, so
363
+ * the saved session is loaded into memory by `create()`, reads come from memory, and
364
+ * writes are saved in the background. Failures are reported through `onError`.
365
+ *
366
+ * Saves carry the revision they were based on. If another tab wrote first, the two
367
+ * are merged: inputs per question, and progress only when this tab saved progress
368
+ * itself, so a tab that only typed cannot overwrite another tab's progress.
369
+ */
370
+ declare class AdapterSessionManager implements SessionStore {
371
+ private readonly adapter;
372
+ private readonly sessionId;
373
+ private readonly available;
374
+ private readonly options;
375
+ private record;
376
+ private revision;
377
+ private readonly dirtyInputs;
378
+ private dirtyProgress;
379
+ private pending;
380
+ private persisting;
381
+ private readonly signal;
382
+ private unsubscribe;
383
+ private constructor();
384
+ /** Loads (or creates) the session id and its saved record, then returns the manager. */
385
+ static create(adapter: StorageAdapter, options?: AdapterSessionManagerOptions): Promise<AdapterSessionManager>;
386
+ getSessionId(): string;
387
+ get isStorageAvailable(): boolean;
388
+ saveProgress(currentPageId: string, answers: AnswerMap): void;
389
+ loadProgress(): SessionProgress | null;
390
+ saveInput(questionId: string, value: InputValue): void;
391
+ loadInput(questionId: string): InputValue | null;
392
+ clearAll(): void;
393
+ dispose(): void;
394
+ /** Resolves when every save made so far has finished. Mostly for tests. */
395
+ flush(): Promise<void>;
396
+ private report;
397
+ private schedulePersist;
398
+ /** Saves until nothing is left to save. Never rejects. */
399
+ private drain;
400
+ private persistOnce;
401
+ /** Merges a version written elsewhere with this tab's unsaved changes. */
402
+ private applyRemote;
403
+ private removeRecord;
236
404
  }
237
405
 
238
- export { type AnswerMap, type BranchingAlways, type BranchingIf, type BranchingRule, BrowserEventAdapter, type ChatConditionRef, ChatController, type ChatControllerConfig, type ChatError, type ChatErrorCode, type ChatEvent, type ChatInputType, type ChatOptions, type ChatPage, type ChatQuestion, type ChatSchema, ConsoleErrorAdapter, type ErrorLogAdapter, type FormCompleteEvent, IdentityInputAdapter, type InputAdapter, type JsonLogicRule, type OutputAdapter, type PageSubmitEvent, RuleEngine, SessionManager, type SessionProgress, type StorageBackend, parseSchema };
406
+ export { AdapterSessionManager, type AdapterSessionManagerOptions, type AnswerMap, type BranchingAlways, type BranchingIf, type BranchingRule, BrowserEventAdapter, type ChatConditionRef, ChatController, type ChatControllerConfig, type ChatError, type ChatErrorCode, type ChatEvent, type ChatInputType, type ChatOptions, type ChatPage, type ChatQuestion, type ChatSchema, ConsoleErrorAdapter, type ErrorLogAdapter, type FormCompleteEvent, type HideAnimation, IdentityInputAdapter, type InputAdapter, type JsonLogicRule, type OutputAdapter, type PageSubmitEvent, type RouteEntry, RuleEngine, SessionManager, type SessionProgress, type SessionStore, type StorageBackend, type SubmitOutcome, parseSchema };