@holdyapp/sdk 0.2.1 → 1.0.1
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/README.md +20 -147
- package/dist/auto-capture.d.ts +12 -16
- package/dist/auto-capture.d.ts.map +1 -0
- package/dist/browser.d.ts +23 -0
- package/dist/browser.d.ts.map +1 -0
- package/dist/core.d.ts +42 -0
- package/dist/core.d.ts.map +1 -0
- package/dist/holdy.js +2 -0
- package/dist/holdy.js.map +7 -0
- package/dist/index.cjs +430 -0
- package/dist/index.cjs.map +7 -0
- package/dist/index.d.ts +5 -46
- package/dist/index.d.ts.map +1 -0
- package/dist/index.mjs +399 -0
- package/dist/index.mjs.map +7 -0
- package/dist/server.cjs +177 -0
- package/dist/server.cjs.map +7 -0
- package/dist/server.d.ts +15 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.mjs +148 -0
- package/dist/server.mjs.map +7 -0
- package/dist/types.d.ts +41 -109
- package/dist/types.d.ts.map +1 -0
- package/dist/web-context.d.ts +51 -0
- package/dist/web-context.d.ts.map +1 -0
- package/dist/web.d.ts +41 -0
- package/dist/web.d.ts.map +1 -0
- package/dist/web.mjs +389 -0
- package/dist/web.mjs.map +7 -0
- package/package.json +24 -20
- package/src/auto-capture.ts +51 -78
- package/src/browser.ts +55 -0
- package/src/core.test.ts +147 -0
- package/src/core.ts +172 -0
- package/src/index.ts +4 -427
- package/src/server.ts +19 -0
- package/src/types.ts +38 -162
- package/src/web-context.test.ts +71 -0
- package/src/web-context.ts +227 -0
- package/src/web.ts +108 -0
- package/LICENSE +0 -21
- package/dist/anchor.d.ts +0 -8
- package/dist/holdy.cjs.js +0 -255
- package/dist/holdy.cjs.js.map +0 -7
- package/dist/holdy.esm.js +0 -255
- package/dist/holdy.esm.js.map +0 -7
- package/dist/holdy.iife.js +0 -255
- package/dist/holdy.iife.js.map +0 -7
- package/dist/outcome.d.ts +0 -19
- package/dist/queue.d.ts +0 -54
- package/dist/render/kinds/banner.d.ts +0 -3
- package/dist/render/kinds/checklist.d.ts +0 -19
- package/dist/render/kinds/cta.d.ts +0 -8
- package/dist/render/kinds/modal.d.ts +0 -16
- package/dist/render/kinds/shared.d.ts +0 -16
- package/dist/render/kinds/spotlight.d.ts +0 -17
- package/dist/render/kinds/tooltip.d.ts +0 -3
- package/dist/render/root.d.ts +0 -3
- package/dist/render/styles.d.ts +0 -5
- package/dist/store.d.ts +0 -36
- package/dist/transport.d.ts +0 -59
- package/src/anchor.ts +0 -72
- package/src/outcome.ts +0 -51
- package/src/queue.ts +0 -296
- package/src/render/kinds/banner.ts +0 -52
- package/src/render/kinds/checklist.ts +0 -175
- package/src/render/kinds/cta.ts +0 -53
- package/src/render/kinds/modal.ts +0 -129
- package/src/render/kinds/shared.ts +0 -67
- package/src/render/kinds/spotlight.ts +0 -176
- package/src/render/kinds/tooltip.ts +0 -82
- package/src/render/root.ts +0 -69
- package/src/render/styles.ts +0 -280
- package/src/store.ts +0 -106
- package/src/transport.ts +0 -182
package/src/store.ts
DELETED
|
@@ -1,106 +0,0 @@
|
|
|
1
|
-
import type { InterventionKind } from './types'
|
|
2
|
-
|
|
3
|
-
interface ActiveSlot {
|
|
4
|
-
decisionId: string
|
|
5
|
-
confidence: number
|
|
6
|
-
kind: InterventionKind
|
|
7
|
-
dismiss: () => void
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* Active intervention slots · prioridade por confidence.
|
|
12
|
-
*
|
|
13
|
-
* Caps globais:
|
|
14
|
-
* - 1 banner global
|
|
15
|
-
* - 1 modal global (Wave 2 · UX bloqueia tudo, único)
|
|
16
|
-
* - 1 checklist global (Wave 2 · sticky bottom-right)
|
|
17
|
-
* - N tooltips, 1 por anchor
|
|
18
|
-
* - N spotlights, 1 por anchor (Wave 2)
|
|
19
|
-
* - 1 CTA global (sticky bottom-right)
|
|
20
|
-
*
|
|
21
|
-
* Modal e checklist + cta competem pelo mesmo "espaço mental" do usuário ·
|
|
22
|
-
* a próxima implementação pode consolidar (1 floating element no canto).
|
|
23
|
-
* Wave 2 mantém slots separados pra simplicidade.
|
|
24
|
-
*
|
|
25
|
-
* Quando 2 decisions reivindicam o mesmo slot, a de menor confidence perde.
|
|
26
|
-
*/
|
|
27
|
-
export class InterventionStore {
|
|
28
|
-
private readonly tooltipsByAnchor = new Map<string, ActiveSlot>()
|
|
29
|
-
private readonly spotlightsByAnchor = new Map<string, ActiveSlot>()
|
|
30
|
-
private banner: ActiveSlot | null = null
|
|
31
|
-
private modal: ActiveSlot | null = null
|
|
32
|
-
private checklist: ActiveSlot | null = null
|
|
33
|
-
private cta: ActiveSlot | null = null
|
|
34
|
-
|
|
35
|
-
/** Tenta reivindicar slot. Retorna previous slot que precisa ser dismissado (caller). */
|
|
36
|
-
claim(
|
|
37
|
-
kind: InterventionKind,
|
|
38
|
-
decisionId: string,
|
|
39
|
-
confidence: number,
|
|
40
|
-
anchorSelector: string | undefined,
|
|
41
|
-
dismiss: () => void,
|
|
42
|
-
): ActiveSlot | null {
|
|
43
|
-
const slot: ActiveSlot = { decisionId, confidence, kind, dismiss }
|
|
44
|
-
|
|
45
|
-
if (kind === 'tooltip') {
|
|
46
|
-
const key = anchorSelector ?? '__no_anchor__'
|
|
47
|
-
const prev = this.tooltipsByAnchor.get(key)
|
|
48
|
-
if (prev && prev.confidence >= confidence) return slot
|
|
49
|
-
this.tooltipsByAnchor.set(key, slot)
|
|
50
|
-
return prev ?? null
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
if (kind === 'spotlight') {
|
|
54
|
-
const key = anchorSelector ?? '__no_anchor__'
|
|
55
|
-
const prev = this.spotlightsByAnchor.get(key)
|
|
56
|
-
if (prev && prev.confidence >= confidence) return slot
|
|
57
|
-
this.spotlightsByAnchor.set(key, slot)
|
|
58
|
-
return prev ?? null
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
if (kind === 'banner') {
|
|
62
|
-
const prev = this.banner
|
|
63
|
-
if (prev && prev.confidence >= confidence) return slot
|
|
64
|
-
this.banner = slot
|
|
65
|
-
return prev ?? null
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
if (kind === 'modal') {
|
|
69
|
-
const prev = this.modal
|
|
70
|
-
if (prev && prev.confidence >= confidence) return slot
|
|
71
|
-
this.modal = slot
|
|
72
|
-
return prev ?? null
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
if (kind === 'checklist') {
|
|
76
|
-
const prev = this.checklist
|
|
77
|
-
if (prev && prev.confidence >= confidence) return slot
|
|
78
|
-
this.checklist = slot
|
|
79
|
-
return prev ?? null
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
if (kind === 'cta') {
|
|
83
|
-
const prev = this.cta
|
|
84
|
-
if (prev && prev.confidence >= confidence) return slot
|
|
85
|
-
this.cta = slot
|
|
86
|
-
return prev ?? null
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
return null
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
release(decisionId: string): void {
|
|
93
|
-
if (this.banner?.decisionId === decisionId) this.banner = null
|
|
94
|
-
if (this.modal?.decisionId === decisionId) this.modal = null
|
|
95
|
-
if (this.checklist?.decisionId === decisionId) this.checklist = null
|
|
96
|
-
if (this.cta?.decisionId === decisionId) this.cta = null
|
|
97
|
-
for (const map of [this.tooltipsByAnchor, this.spotlightsByAnchor]) {
|
|
98
|
-
for (const [k, slot] of map) {
|
|
99
|
-
if (slot.decisionId === decisionId) {
|
|
100
|
-
map.delete(k)
|
|
101
|
-
break
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
}
|
package/src/transport.ts
DELETED
|
@@ -1,182 +0,0 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
AccountIdentity,
|
|
3
|
-
CanonicalPrimitive,
|
|
4
|
-
DecisionResult,
|
|
5
|
-
HoldyConfig,
|
|
6
|
-
RuntimeConfig,
|
|
7
|
-
UserIdentity,
|
|
8
|
-
} from './types'
|
|
9
|
-
import { EventQueue } from './queue'
|
|
10
|
-
|
|
11
|
-
const DEFAULT_API_URL = 'https://app.holdy.com.br/api/v1'
|
|
12
|
-
|
|
13
|
-
export class Transport {
|
|
14
|
-
private readonly apiKey: string
|
|
15
|
-
private readonly apiUrl: string
|
|
16
|
-
private readonly debug: boolean
|
|
17
|
-
private readonly queue: EventQueue
|
|
18
|
-
|
|
19
|
-
constructor(config: HoldyConfig) {
|
|
20
|
-
this.apiKey = config.apiKey
|
|
21
|
-
this.apiUrl = (config.apiUrl ?? DEFAULT_API_URL).replace(/\/$/, '')
|
|
22
|
-
this.debug = config.debug ?? false
|
|
23
|
-
this.queue = new EventQueue(
|
|
24
|
-
(body) => this.sendBatch(body.events),
|
|
25
|
-
(...args) => this.log(...args),
|
|
26
|
-
)
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
private headers(): Record<string, string> {
|
|
30
|
-
return {
|
|
31
|
-
'content-type': 'application/json',
|
|
32
|
-
authorization: `Bearer ${this.apiKey}`,
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
async fetchRuntimeConfig(): Promise<RuntimeConfig | null> {
|
|
37
|
-
try {
|
|
38
|
-
const res = await fetch(`${this.apiUrl}/runtime/config`, {
|
|
39
|
-
method: 'GET',
|
|
40
|
-
headers: this.headers(),
|
|
41
|
-
credentials: 'omit',
|
|
42
|
-
mode: 'cors',
|
|
43
|
-
})
|
|
44
|
-
if (!res.ok) {
|
|
45
|
-
this.log('runtime/config failed', res.status)
|
|
46
|
-
return null
|
|
47
|
-
}
|
|
48
|
-
return (await res.json()) as RuntimeConfig
|
|
49
|
-
} catch (e) {
|
|
50
|
-
this.log('runtime/config error', e)
|
|
51
|
-
return null
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
async evaluate(input: {
|
|
56
|
-
account_id: string
|
|
57
|
-
current_workflow?: string
|
|
58
|
-
recent_primitives?: Array<{
|
|
59
|
-
primitive: CanonicalPrimitive
|
|
60
|
-
payload: Record<string, unknown>
|
|
61
|
-
occurred_at: string
|
|
62
|
-
}>
|
|
63
|
-
recent_sentiment?: 'positivo' | 'neutro' | 'negativo' | 'urgente'
|
|
64
|
-
consumer?: 'ui' | 'csm' | 'agent'
|
|
65
|
-
}): Promise<DecisionResult | null> {
|
|
66
|
-
try {
|
|
67
|
-
const res = await fetch(`${this.apiUrl}/decisions/evaluate`, {
|
|
68
|
-
method: 'POST',
|
|
69
|
-
headers: this.headers(),
|
|
70
|
-
body: JSON.stringify({ consumer: 'ui', ...input }),
|
|
71
|
-
credentials: 'omit',
|
|
72
|
-
mode: 'cors',
|
|
73
|
-
})
|
|
74
|
-
if (!res.ok) {
|
|
75
|
-
this.log('decisions/evaluate failed', res.status)
|
|
76
|
-
return null
|
|
77
|
-
}
|
|
78
|
-
return (await res.json()) as DecisionResult
|
|
79
|
-
} catch (e) {
|
|
80
|
-
this.log('decisions/evaluate error', e)
|
|
81
|
-
return null
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
/**
|
|
86
|
-
* Event ingest · delegado pra EventQueue (IndexedDB + exponential backoff).
|
|
87
|
-
* Tenta envio imediato; se falhar, persiste e retry em background.
|
|
88
|
-
*/
|
|
89
|
-
ingestBatch(events: Array<{
|
|
90
|
-
primitive: CanonicalPrimitive
|
|
91
|
-
occurred_at: string
|
|
92
|
-
account_id?: string
|
|
93
|
-
user_id?: string
|
|
94
|
-
session_id?: string
|
|
95
|
-
payload: Record<string, unknown>
|
|
96
|
-
idempotency_key?: string
|
|
97
|
-
}>): void {
|
|
98
|
-
if (events.length === 0) return
|
|
99
|
-
void this.queue.enqueue(events)
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
/**
|
|
103
|
-
* Send raw · usado pelo queue. Retorna true em sucesso (2xx), false em
|
|
104
|
-
* falha (rede ou 4xx/5xx).
|
|
105
|
-
*
|
|
106
|
-
* IMPORTANTE: 4xx é tratado como falha permanente — não vale retry. Mas
|
|
107
|
-
* a fila não distingue (mantém pra retry simples). MAX_ATTEMPTS = 5 limita
|
|
108
|
-
* o desperdício. Refinamento (4xx → drop imediato) fica pra Wave 3.
|
|
109
|
-
*/
|
|
110
|
-
private async sendBatch(events: unknown[]): Promise<boolean> {
|
|
111
|
-
try {
|
|
112
|
-
const res = await fetch(`${this.apiUrl}/events/batch`, {
|
|
113
|
-
method: 'POST',
|
|
114
|
-
headers: this.headers(),
|
|
115
|
-
body: JSON.stringify({ events }),
|
|
116
|
-
credentials: 'omit',
|
|
117
|
-
mode: 'cors',
|
|
118
|
-
keepalive: true,
|
|
119
|
-
})
|
|
120
|
-
return res.ok
|
|
121
|
-
} catch (e) {
|
|
122
|
-
this.log('events/batch network error', e)
|
|
123
|
-
return false
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
/**
|
|
128
|
-
* Upsert account + opcional user via /v1/accounts/upsert.
|
|
129
|
-
*
|
|
130
|
-
* Best-effort fire-and-forget. Backend COALESCE não sobrescreve campos não-nulos.
|
|
131
|
-
* Retorna account_id (UUID Holdy) pra hidratar config local; null em falha.
|
|
132
|
-
*/
|
|
133
|
-
async upsertAccount(payload: {
|
|
134
|
-
account: AccountIdentity
|
|
135
|
-
user?: UserIdentity
|
|
136
|
-
}): Promise<{ account_id: string; account_user_id: string | null } | null> {
|
|
137
|
-
try {
|
|
138
|
-
const res = await fetch(`${this.apiUrl}/accounts/upsert`, {
|
|
139
|
-
method: 'POST',
|
|
140
|
-
headers: this.headers(),
|
|
141
|
-
body: JSON.stringify(payload),
|
|
142
|
-
credentials: 'omit',
|
|
143
|
-
mode: 'cors',
|
|
144
|
-
keepalive: true,
|
|
145
|
-
})
|
|
146
|
-
if (!res.ok) {
|
|
147
|
-
this.log('accounts/upsert failed', res.status)
|
|
148
|
-
return null
|
|
149
|
-
}
|
|
150
|
-
return (await res.json()) as { account_id: string; account_user_id: string | null }
|
|
151
|
-
} catch (e) {
|
|
152
|
-
this.log('accounts/upsert error', e)
|
|
153
|
-
return null
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
reportOutcome(
|
|
158
|
-
decisionId: string,
|
|
159
|
-
outcome: 'executed' | 'dismissed' | 'converted' | 'no_show' | 'failed',
|
|
160
|
-
metadata?: Record<string, unknown>,
|
|
161
|
-
): void {
|
|
162
|
-
const url = `${this.apiUrl}/decisions/${encodeURIComponent(decisionId)}/outcome`
|
|
163
|
-
fetch(url, {
|
|
164
|
-
method: 'POST',
|
|
165
|
-
headers: this.headers(),
|
|
166
|
-
body: JSON.stringify({ outcome, metadata }),
|
|
167
|
-
credentials: 'omit',
|
|
168
|
-
mode: 'cors',
|
|
169
|
-
keepalive: true,
|
|
170
|
-
}).catch((e) => this.log('decisions/outcome error', e))
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
private log(...args: unknown[]): void {
|
|
174
|
-
if (this.debug && typeof console !== 'undefined') {
|
|
175
|
-
console.warn('[holdy]', ...args)
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
destroy(): void {
|
|
180
|
-
this.queue.destroy()
|
|
181
|
-
}
|
|
182
|
-
}
|