handhold-sdk 1.0.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.
Files changed (50) hide show
  1. package/README.md +427 -0
  2. package/dist/d44f1fe8001e32f8e74d.svg +3056 -0
  3. package/dist/fonts/Phosphor.ttf +0 -0
  4. package/dist/fonts/Phosphor.woff +0 -0
  5. package/dist/fonts/Phosphor.woff2 +0 -0
  6. package/dist/handhold.min.js +1 -0
  7. package/package.json +114 -0
  8. package/src/HandHold.js +1599 -0
  9. package/src/agent/VENDOR.md +203 -0
  10. package/src/agent/agent/AgentLoop.js +366 -0
  11. package/src/agent/agent/prompts.js +163 -0
  12. package/src/agent/agent/tools.js +148 -0
  13. package/src/agent/controller.js +235 -0
  14. package/src/agent/dom/actions.js +456 -0
  15. package/src/agent/dom/domTree.js +1761 -0
  16. package/src/agent/dom/redact.js +98 -0
  17. package/src/agent/dom/serializer.js +332 -0
  18. package/src/agent/dom/utils.js +99 -0
  19. package/src/agent/index.js +169 -0
  20. package/src/agent/llm/connector.js +143 -0
  21. package/src/agent/package.json +3 -0
  22. package/src/agent/policy/PolicyGuard.js +172 -0
  23. package/src/agent/site/manifest.js +145 -0
  24. package/src/agent/ui/ChatWidget.js +219 -0
  25. package/src/agent-mode/AgentMode.js +429 -0
  26. package/src/api/APIClient.js +258 -0
  27. package/src/core/Config.js +207 -0
  28. package/src/core/UiState.js +83 -0
  29. package/src/core/defaults.js +20 -0
  30. package/src/events.js +59 -0
  31. package/src/index.js +77 -0
  32. package/src/intent/ActionVerbMap.js +324 -0
  33. package/src/intent/IntentExtractor.js +317 -0
  34. package/src/observers/ElementScanner.js +284 -0
  35. package/src/observers/NavigationObserver.js +182 -0
  36. package/src/observers/PageObserver.js +293 -0
  37. package/src/session/SessionManager.js +402 -0
  38. package/src/session/SessionStorage.js +148 -0
  39. package/src/ui/HelpWidget.js +1189 -0
  40. package/src/ui/UICoach.js +1725 -0
  41. package/src/ui/components/Button.css +137 -0
  42. package/src/ui/components/Button.js +102 -0
  43. package/src/ui/widget.css +599 -0
  44. package/src/utils/Logger.js +132 -0
  45. package/src/utils/MarkdownRenderer.js +39 -0
  46. package/src/utils/domUtils.js +350 -0
  47. package/src/utils/eventBus.js +102 -0
  48. package/src/utils/index.js +8 -0
  49. package/src/utils/stringUtils.js +202 -0
  50. package/types/index.d.ts +538 -0
@@ -0,0 +1,207 @@
1
+ /**
2
+ * Hand Hold SDK - Configuration Management
3
+ * Handles SDK configuration with defaults and validation
4
+ */
5
+
6
+ import { DEFAULT_BASE_URL } from './defaults.js';
7
+
8
+ const DEFAULT_CONFIG = {
9
+ // Required
10
+ apiKey: null,
11
+ baseUrl: null,
12
+
13
+ // UI Customization
14
+ theme: 'light', // 'light' | 'dark'
15
+ position: 'bottom-right', // 'bottom-right' | 'bottom-left' | 'top-right' | 'top-left'
16
+ primaryColor: '#004AF5', // Primary accent color (PaidHR Blue 500)
17
+ zIndex: 999990, // Base z-index for SDK elements
18
+
19
+ // Behavior
20
+ minHighlightConfidence: 0.70, // Minimum confidence to highlight element
21
+ sessionInactivityTimeout: 600000, // Session timeout (10 minutes)
22
+ autoOpenOnLoad: false, // Auto-open help widget on page load
23
+ deferRestore: false, // If true, init() skips session restore; the host calls
24
+ // HandHold.restoreSession() after patching its own renderer
25
+ // (Phase 1.4 — avoids the legacy widget drawing the confirm)
26
+ enableMultiPageTracking: true, // Track guidance across pages
27
+ showHelpButton: true, // Show floating help button
28
+ showStepHighlight: true, // Draw the SDK's own overlay/arrow/tooltip on the
29
+ // current step's element. Turn OFF when the host
30
+ // renders its own on-page indicator (avoids a
31
+ // duplicate highlight); completion is still detected
32
+ // — the SDK watches the real element either way.
33
+ agentMode: false, // "Do it for me" agent (H5) — off unless the tenant enables it
34
+ enableScreenshots: true, // Capture a page screenshot (html2canvas) for vision intent
35
+ // extraction. Turn OFF on sites using modern CSS colors
36
+ // (oklch/oklab, e.g. Tailwind v4): html2canvas can't parse
37
+ // them and stalls for seconds before failing. The agent
38
+ // never needs screenshots (it uses the DOM engine).
39
+
40
+ // API
41
+ apiTimeout: 30000, // API request timeout (30 seconds)
42
+ retryAttempts: 3, // Number of retry attempts
43
+ retryDelay: 1000, // Delay between retries
44
+
45
+ // Debug
46
+ debug: false, // Enable debug mode
47
+ logLevel: 'warn', // 'error' | 'warn' | 'info' | 'debug'
48
+
49
+ // Help Button
50
+ helpButtonText: '?', // Text/icon for help button
51
+ helpButtonAriaLabel: 'Open help', // Accessibility label
52
+
53
+ // Widget
54
+ widgetTitle: 'How can I help?', // Widget title
55
+ widgetPlaceholder: 'Type your question...', // Search input placeholder
56
+ maxIntents: 6, // Maximum intents to show
57
+ };
58
+
59
+ class Config {
60
+ constructor(userConfig = {}) {
61
+ // Validate required fields
62
+ if (!userConfig.apiKey) {
63
+ throw new Error('[HandHold Config] apiKey is required');
64
+ }
65
+ // baseUrl may come from the call, or the build-time baked default
66
+ // (HANDHOLD_DEFAULT_BASE_URL). Only an unresolvable baseUrl is an error.
67
+ const resolvedBaseUrl = userConfig.baseUrl || DEFAULT_BASE_URL;
68
+ if (!resolvedBaseUrl) {
69
+ throw new Error(
70
+ '[HandHold Config] baseUrl is required — pass baseUrl, set data-handhold-base-url, ' +
71
+ 'or build the SDK with HANDHOLD_DEFAULT_BASE_URL'
72
+ );
73
+ }
74
+
75
+ // Merge with defaults
76
+ this._config = { ...DEFAULT_CONFIG, ...userConfig, baseUrl: resolvedBaseUrl };
77
+
78
+ // Normalize baseUrl (remove trailing slash)
79
+ this._config.baseUrl = this._config.baseUrl.replace(/\/$/, '');
80
+
81
+ // Validate values
82
+ this._validate();
83
+
84
+ // Freeze config to prevent modification
85
+ Object.freeze(this._config);
86
+ }
87
+
88
+ /**
89
+ * Validate configuration values
90
+ */
91
+ _validate() {
92
+ const { theme, position, logLevel, minHighlightConfidence } = this._config;
93
+
94
+ // Validate theme
95
+ if (!['light', 'dark'].includes(theme)) {
96
+ console.warn(`[HandHold Config] Invalid theme "${theme}", using "light"`);
97
+ this._config.theme = 'light';
98
+ }
99
+
100
+ // Validate position
101
+ const validPositions = ['bottom-right', 'bottom-left', 'top-right', 'top-left'];
102
+ if (!validPositions.includes(position)) {
103
+ console.warn(`[HandHold Config] Invalid position "${position}", using "bottom-right"`);
104
+ this._config.position = 'bottom-right';
105
+ }
106
+
107
+ // Validate log level
108
+ const validLogLevels = ['error', 'warn', 'info', 'debug'];
109
+ if (!validLogLevels.includes(logLevel)) {
110
+ console.warn(`[HandHold Config] Invalid logLevel "${logLevel}", using "warn"`);
111
+ this._config.logLevel = 'warn';
112
+ }
113
+
114
+ // Validate confidence threshold
115
+ if (minHighlightConfidence < 0 || minHighlightConfidence > 1) {
116
+ console.warn(`[HandHold Config] minHighlightConfidence must be between 0 and 1, using 0.85`);
117
+ this._config.minHighlightConfidence = 0.85;
118
+ }
119
+ }
120
+
121
+ /**
122
+ * Get configuration value
123
+ * @param {string} key - Config key
124
+ * @returns {*} - Config value
125
+ */
126
+ get(key) {
127
+ return this._config[key];
128
+ }
129
+
130
+ /**
131
+ * Get all configuration as object
132
+ * @returns {Object}
133
+ */
134
+ getAll() {
135
+ return { ...this._config };
136
+ }
137
+
138
+ /**
139
+ * Check if debug mode is enabled
140
+ * @returns {boolean}
141
+ */
142
+ get isDebug() {
143
+ return this._config.debug;
144
+ }
145
+
146
+ // Convenience getters
147
+ get apiKey() { return this._config.apiKey; }
148
+ get baseUrl() { return this._config.baseUrl; }
149
+ get theme() { return this._config.theme; }
150
+ get position() { return this._config.position; }
151
+ get primaryColor() { return this._config.primaryColor; }
152
+ get zIndex() { return this._config.zIndex; }
153
+ get agentMode() { return this._config.agentMode; }
154
+ get minHighlightConfidence() { return this._config.minHighlightConfidence; }
155
+ get sessionInactivityTimeout() { return this._config.sessionInactivityTimeout; }
156
+ get enableMultiPageTracking() { return this._config.enableMultiPageTracking; }
157
+ get logLevel() { return this._config.logLevel; }
158
+ get maxIntents() { return this._config.maxIntents; }
159
+ get showHelpButton() { return this._config.showHelpButton; }
160
+ get apiTimeout() { return this._config.apiTimeout; }
161
+ get retryAttempts() { return this._config.retryAttempts; }
162
+ get retryDelay() { return this._config.retryDelay; }
163
+ get widgetTitle() { return this._config.widgetTitle; }
164
+ get widgetPlaceholder() { return this._config.widgetPlaceholder; }
165
+ get helpButtonText() { return this._config.helpButtonText; }
166
+ get helpButtonAriaLabel() { return this._config.helpButtonAriaLabel; }
167
+ get autoOpenOnLoad() { return this._config.autoOpenOnLoad; }
168
+
169
+ /**
170
+ * Get CSS variables for theming
171
+ * @returns {Object}
172
+ */
173
+ getCSSVariables() {
174
+ const isDark = this._config.theme === 'dark';
175
+
176
+ return {
177
+ '--hh-primary': this._config.primaryColor,
178
+ '--hh-primary-light': this._adjustColor(this._config.primaryColor, 40),
179
+ '--hh-primary-dark': this._adjustColor(this._config.primaryColor, -40),
180
+ '--hh-bg': isDark ? '#1e1e1e' : '#ffffff',
181
+ '--hh-bg-secondary': isDark ? '#2d2d2d' : '#f5f5f5',
182
+ '--hh-text': isDark ? '#ffffff' : '#333333',
183
+ '--hh-text-secondary': isDark ? '#b0b0b0' : '#666666',
184
+ '--hh-border': isDark ? '#404040' : '#e0e0e0',
185
+ '--hh-shadow': isDark ? 'rgba(0, 0, 0, 0.5)' : 'rgba(0, 0, 0, 0.15)',
186
+ '--hh-overlay': 'rgba(0, 0, 0, 0.5)',
187
+ '--hh-z-index': this._config.zIndex,
188
+ };
189
+ }
190
+
191
+ /**
192
+ * Adjust color brightness
193
+ * @param {string} color - Hex color
194
+ * @param {number} amount - Amount to adjust (-255 to 255)
195
+ * @returns {string}
196
+ */
197
+ _adjustColor(color, amount) {
198
+ const hex = color.replace('#', '');
199
+ const r = Math.max(0, Math.min(255, parseInt(hex.substr(0, 2), 16) + amount));
200
+ const g = Math.max(0, Math.min(255, parseInt(hex.substr(2, 2), 16) + amount));
201
+ const b = Math.max(0, Math.min(255, parseInt(hex.substr(4, 2), 16) + amount));
202
+ return `#${r.toString(16).padStart(2, '0')}${g.toString(16).padStart(2, '0')}${b.toString(16).padStart(2, '0')}`;
203
+ }
204
+ }
205
+
206
+ export default Config;
207
+ export { DEFAULT_CONFIG };
@@ -0,0 +1,83 @@
1
+ /**
2
+ * Hand Hold SDK - UiState (Phase 2.1)
3
+ *
4
+ * The single source of truth for the widget UI. A tiny observable store shaped
5
+ * for React's useSyncExternalStore: getSnapshot() returns a STABLE object
6
+ * reference until setState() replaces it, and subscribe(fn) is notified on every
7
+ * change. The legacy DOM widget and any host renderer (the React demo) both read
8
+ * from this one store, so the two can no longer drift.
9
+ *
10
+ * State shape (all serializable):
11
+ * view 'menu' | 'loading' | 'guidance' | 'article' | 'error'
12
+ * | 'clarification' | 'completion'
13
+ * isOpen whether the widget panel is open
14
+ * intents scanned intent list for the menu
15
+ * guidance active guidance object (steps, current_step, agent_plan, …)
16
+ * article KB article being shown
17
+ * error error message string
18
+ * clarification { question, options } for an ambiguous query
19
+ * completion completion label
20
+ * loadingLabel label for the loading view
21
+ * confirm { message } for the session-restore confirm
22
+ */
23
+
24
+ const DEFAULT_STATE = {
25
+ view: 'menu',
26
+ isOpen: false,
27
+ intents: [],
28
+ guidance: null,
29
+ article: null,
30
+ error: null,
31
+ clarification: null,
32
+ completion: null,
33
+ loadingLabel: null,
34
+ confirm: null,
35
+ };
36
+
37
+ export default class UiState {
38
+ constructor(initial = {}) {
39
+ this._state = { ...DEFAULT_STATE, ...initial };
40
+ this._listeners = new Set();
41
+ // Bound so it can be passed directly as useSyncExternalStore(subscribe, …).
42
+ this.subscribe = this.subscribe.bind(this);
43
+ this.getSnapshot = this.getSnapshot.bind(this);
44
+ }
45
+
46
+ /** Current immutable snapshot (stable reference until setState). */
47
+ getSnapshot() {
48
+ return this._state;
49
+ }
50
+
51
+ /**
52
+ * Subscribe to state changes. The listener is called with the new snapshot.
53
+ * @returns {() => void} unsubscribe
54
+ */
55
+ subscribe(listener) {
56
+ this._listeners.add(listener);
57
+ return () => this._listeners.delete(listener);
58
+ }
59
+
60
+ /** Merge a partial update, producing a new snapshot, and notify listeners. */
61
+ setState(partial) {
62
+ this._state = { ...this._state, ...partial };
63
+ this._notify();
64
+ }
65
+
66
+ /** Reset to defaults (e.g. on close/destroy). */
67
+ reset() {
68
+ this._state = { ...DEFAULT_STATE };
69
+ this._notify();
70
+ }
71
+
72
+ _notify() {
73
+ for (const listener of this._listeners) {
74
+ // One bad subscriber must not break the others (or the state update).
75
+ try {
76
+ listener(this._state);
77
+ } catch (e) {
78
+ // eslint-disable-next-line no-console
79
+ console.error('[HandHold UiState] subscriber threw:', e);
80
+ }
81
+ }
82
+ }
83
+ }
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Hand Hold SDK - Build-time defaults
3
+ *
4
+ * The default backend URL is settable by whoever builds the bundle. A Model B
5
+ * self-hoster runs:
6
+ *
7
+ * HANDHOLD_DEFAULT_BASE_URL=https://help.acme.com npm run build
8
+ *
9
+ * and the served `handhold.js` already points at THEIR backend — so their pages
10
+ * only need `data-handhold-api-key`, not `data-handhold-base-url` on every embed.
11
+ *
12
+ * `__HANDHOLD_DEFAULT_BASE_URL__` is replaced at build time by webpack's
13
+ * DefinePlugin. The `typeof` guard keeps this safe under jest/node (where the
14
+ * global is not defined) — there it resolves to '' and callers fall back to an
15
+ * explicit value or same-origin.
16
+ */
17
+ export const DEFAULT_BASE_URL =
18
+ (typeof __HANDHOLD_DEFAULT_BASE_URL__ !== 'undefined' && __HANDHOLD_DEFAULT_BASE_URL__) || '';
19
+
20
+ export default DEFAULT_BASE_URL;
package/src/events.js ADDED
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Hand Hold SDK - Event registry (Phase 6.3)
3
+ *
4
+ * One place that names every EventBus event. Stringly-typed event names caused
5
+ * real bugs (an unhandled 'ui:highlight_request', a missing 'session:close'
6
+ * wiring): a typo in an emit or on had no compile-time signal. Reference these
7
+ * constants instead of raw strings so emit and on always agree.
8
+ *
9
+ * Values are IDENTICAL to the historical string literals, so this can be
10
+ * adopted incrementally — code still using a raw string interops with code
11
+ * using the constant.
12
+ */
13
+ export const EVENTS = Object.freeze({
14
+ // Lifecycle
15
+ READY: 'handhold:ready',
16
+ CLOSED: 'handhold:closed',
17
+ STATE_CHANGED: 'state:changed',
18
+
19
+ // User/host commands (host -> SDK)
20
+ HELP_OPEN: 'help:open',
21
+ QUERY_SUBMITTED: 'query:submitted',
22
+ INTENT_SELECTED: 'intent:selected',
23
+ STEP_DONE: 'step:done',
24
+ STEP_SKIP: 'step:skip',
25
+ STEP_BACK: 'step:back',
26
+ SESSION_CLOSE: 'session:close',
27
+ HELP_MINIMIZE: 'help:minimize',
28
+ HELP_SHOW_HINT: 'help:show_hint',
29
+ CONFIRM_RESPONSE: 'hh:confirm:response',
30
+
31
+ // Page / navigation
32
+ NAVIGATION_CHANGED: 'navigation:changed',
33
+ PAGE_DOM_CHANGED: 'page:dom_changed',
34
+ STEP_SHOW_UI: 'step:show_ui',
35
+ SELECTION_EXPLAIN: 'selection:explain',
36
+ UI_HIGHLIGHT_REQUEST: 'ui:highlight_request',
37
+
38
+ // Highlighting (UICoach)
39
+ HIGHLIGHT_SHOWN: 'highlight:shown',
40
+ HIGHLIGHT_NOT_FOUND: 'highlight:not_found',
41
+
42
+ // Session lifecycle (SessionManager)
43
+ SESSION_STARTED: 'session:started',
44
+ SESSION_UPDATED: 'session:updated',
45
+ SESSION_NAVIGATION: 'session:navigation',
46
+ SESSION_COMPLETED: 'session:completed',
47
+ SESSION_STEP_ADVANCED: 'session:step_advanced',
48
+ SESSION_ENDED: 'session:ended',
49
+
50
+ // Agent mode
51
+ AGENT_START: 'agent:start',
52
+ AGENT_STOP: 'agent:stop',
53
+ AGENT_ACTIVITY: 'agent:activity',
54
+ AGENT_CONFIRM: 'agent:confirm',
55
+ AGENT_ASK: 'agent:ask',
56
+ AGENT_DONE: 'agent:done',
57
+ });
58
+
59
+ export default EVENTS;
package/src/index.js ADDED
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Hand Hold SDK - Entry Point
3
+ *
4
+ * Context-aware, in-app help system that provides:
5
+ * - Intent-based help menus
6
+ * - Visual UI guidance with element highlighting
7
+ * - Multi-page workflow tracking
8
+ * - Conversational fallback for free-text queries
9
+ *
10
+ * @version 1.0.0
11
+ * @license MIT
12
+ */
13
+
14
+ import HandHold, { HandHold as HandHoldClass } from './HandHold.js';
15
+ import { DEFAULT_BASE_URL } from './core/defaults.js';
16
+
17
+ // Export core modules for advanced usage
18
+ export { default as Config } from './core/Config.js';
19
+ export { default as Logger } from './utils/Logger.js';
20
+ export { default as EventBus } from './utils/eventBus.js';
21
+ export { default as APIClient } from './api/APIClient.js';
22
+ export { default as PageObserver } from './observers/PageObserver.js';
23
+ export { default as NavigationObserver } from './observers/NavigationObserver.js';
24
+ export { default as ElementScanner } from './observers/ElementScanner.js';
25
+ export { default as IntentExtractor } from './intent/IntentExtractor.js';
26
+ export { default as ActionVerbMap } from './intent/ActionVerbMap.js';
27
+ export { default as SessionManager } from './session/SessionManager.js';
28
+ export { default as SessionStorage } from './session/SessionStorage.js';
29
+ export { default as HelpWidget } from './ui/HelpWidget.js';
30
+ export { default as UICoach } from './ui/UICoach.js';
31
+
32
+ // Export utilities
33
+ export * as domUtils from './utils/domUtils.js';
34
+ export * as stringUtils from './utils/stringUtils.js';
35
+
36
+ // Event registry + UI-state store (Phase 2 / 6.3)
37
+ export { default as EVENTS } from './events.js';
38
+ export { default as UiState } from './core/UiState.js';
39
+
40
+ // Export HandHold class for custom instantiation
41
+ export { HandHoldClass };
42
+
43
+ // Default export is the singleton instance
44
+ export default HandHold;
45
+
46
+ // Attach to window for non-module usage
47
+ if (typeof window !== 'undefined') {
48
+ window.HandHold = HandHold;
49
+
50
+ // Auto-initialize if data attributes are present
51
+ window.addEventListener('DOMContentLoaded', () => {
52
+ const script = document.querySelector('script[data-handhold-api-key]');
53
+
54
+ if (script) {
55
+ const apiKey = script.getAttribute('data-handhold-api-key');
56
+ // Backend URL precedence: explicit attribute > build-time baked default
57
+ // (HANDHOLD_DEFAULT_BASE_URL) > same-origin (works when the API is reverse
58
+ // -proxied under the app's domain). No dead external default.
59
+ const baseUrl = script.getAttribute('data-handhold-base-url')
60
+ || DEFAULT_BASE_URL
61
+ || (typeof window !== 'undefined' ? window.location.origin : '');
62
+ const theme = script.getAttribute('data-handhold-theme') || 'light';
63
+ const position = script.getAttribute('data-handhold-position') || 'bottom-right';
64
+ const debug = script.getAttribute('data-handhold-debug') === 'true';
65
+
66
+ HandHold.init({
67
+ apiKey,
68
+ baseUrl,
69
+ theme,
70
+ position,
71
+ debug
72
+ }).catch(error => {
73
+ console.error('[HandHold] Auto-initialization failed:', error);
74
+ });
75
+ }
76
+ });
77
+ }