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