@origonai/web-chat-sdk 0.1.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/README.md +423 -0
- package/dist/origon-chat-sdk.js +584 -0
- package/dist/origon-chat-sdk.js.map +1 -0
- package/package.json +51 -0
- package/src/call.js +622 -0
- package/src/chat.js +376 -0
- package/src/constants.js +10 -0
- package/src/http.js +122 -0
- package/src/index.js +58 -0
- package/src/utils.js +84 -0
package/src/chat.js
ADDED
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Chat Service for Chat SDK
|
|
3
|
+
* Handles real-time chat functionality without depending on external state
|
|
4
|
+
* Uses callbacks to communicate state changes to the consumer
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { fetchEventSource } from '@microsoft/fetch-event-source'
|
|
8
|
+
import { getMessages, authenticate } from './http.js'
|
|
9
|
+
import { getDeviceId, sleep } from './utils.js'
|
|
10
|
+
import { MESSAGE_ROLES } from './constants.js'
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* @typedef {Object} ChatCallbacks
|
|
14
|
+
* @property {(messages: Array) => void} [onMessagesUpdate] - Called when messages array should be updated
|
|
15
|
+
* @property {(isTyping: boolean) => void} [onTyping] - Called when typing status changes
|
|
16
|
+
* @property {(isLiveAgent: boolean) => void} [onLiveAgentMode] - Called when live agent mode status changes
|
|
17
|
+
* @property {(sessionId: string) => void} [onSessionUpdate] - Called when session ID is updated
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* @typedef {Object} ChatSession
|
|
22
|
+
* @property {string} sessionId
|
|
23
|
+
* @property {string} sseUrl
|
|
24
|
+
* @property {string} [requestId]
|
|
25
|
+
* @property {boolean} liveAgent
|
|
26
|
+
* @property {AbortController} [abortController]
|
|
27
|
+
* @property {string} [lastStreamId]
|
|
28
|
+
* @property {Array} messages
|
|
29
|
+
* @property {ChatCallbacks} callbacks
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Create a new chat session
|
|
34
|
+
* @param {ChatCallbacks} [callbacks={}]
|
|
35
|
+
* @returns {ChatSession}
|
|
36
|
+
*/
|
|
37
|
+
function createSession(callbacks = {}) {
|
|
38
|
+
return {
|
|
39
|
+
credentials: undefined,
|
|
40
|
+
authenticated: false,
|
|
41
|
+
configData: undefined,
|
|
42
|
+
sessionId: undefined,
|
|
43
|
+
requestId: undefined,
|
|
44
|
+
sseUrl: undefined,
|
|
45
|
+
abortController: undefined,
|
|
46
|
+
liveAgent: false,
|
|
47
|
+
lastStreamId: undefined,
|
|
48
|
+
messages: [],
|
|
49
|
+
callbacks
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** @type {ChatSession} */
|
|
54
|
+
let currentSession = createSession()
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Set callbacks for the current session
|
|
58
|
+
* @param {ChatCallbacks} callbacks
|
|
59
|
+
*/
|
|
60
|
+
export function setCallbacks(callbacks) {
|
|
61
|
+
currentSession.callbacks = { ...currentSession.callbacks, ...callbacks }
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Initialize the chat session
|
|
66
|
+
* @param {Object} credentials - Credentials for the chat
|
|
67
|
+
*/
|
|
68
|
+
export function initialize(credentials) {
|
|
69
|
+
console.log('Initializing chat...', credentials)
|
|
70
|
+
currentSession.credentials = credentials
|
|
71
|
+
if (credentials.token) {
|
|
72
|
+
currentSession.authenticated = true
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Get current chat session credentials
|
|
78
|
+
* @returns {{ endpoint: string, apiKey: string } | undefined}
|
|
79
|
+
*/
|
|
80
|
+
export function getCredentials() {
|
|
81
|
+
return currentSession.credentials
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Update the session ID and notify via callback
|
|
86
|
+
* @param {string} sessionId - The new session ID
|
|
87
|
+
*/
|
|
88
|
+
export function updateSessionId(sessionId) {
|
|
89
|
+
if (sessionId && sessionId !== currentSession.sessionId) {
|
|
90
|
+
currentSession.sessionId = sessionId
|
|
91
|
+
currentSession.callbacks.onSessionUpdate?.(sessionId)
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Initiate a new chat session or resume an existing one
|
|
97
|
+
* @param {Object} credentials - Credentials for the chat
|
|
98
|
+
* @param {Object} payload - Payload for the chat. It contains sessionId (optional)
|
|
99
|
+
* @param {string} [payload.sessionId] - Optional session ID to resume
|
|
100
|
+
* @returns {Promise<{ sessionId: string, messages: Array }>}
|
|
101
|
+
*/
|
|
102
|
+
export async function startChat(payload = {}) {
|
|
103
|
+
try {
|
|
104
|
+
console.log('startChat: ', payload, currentSession)
|
|
105
|
+
|
|
106
|
+
let configData = null
|
|
107
|
+
if (!currentSession.authenticated) {
|
|
108
|
+
configData = await authenticate(currentSession.credentials)
|
|
109
|
+
currentSession.authenticated = true
|
|
110
|
+
currentSession.configData = configData
|
|
111
|
+
} else {
|
|
112
|
+
configData = currentSession.configData
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
let messages = []
|
|
116
|
+
|
|
117
|
+
if (payload.sessionId) {
|
|
118
|
+
const messagesRes = await getMessages(payload.sessionId)
|
|
119
|
+
messages = (messagesRes?.sessionHistory ?? []).map((msg) => ({
|
|
120
|
+
id: msg.id,
|
|
121
|
+
text: msg.text,
|
|
122
|
+
role: msg.youtubeVideo
|
|
123
|
+
? MESSAGE_ROLES.BOT // for youtube video messages, role is "system" from backend, we need to make it "assistant"
|
|
124
|
+
: msg.role,
|
|
125
|
+
timestamp: msg.timestamp,
|
|
126
|
+
video: msg.youtubeVideo,
|
|
127
|
+
channel: msg.channel,
|
|
128
|
+
done: true
|
|
129
|
+
}))
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const searchParams = new URLSearchParams({
|
|
133
|
+
externalId: getExternalId()
|
|
134
|
+
})
|
|
135
|
+
currentSession.sseUrl = `${currentSession.credentials.endpoint}?${searchParams.toString()}`
|
|
136
|
+
currentSession.sessionId = payload.sessionId
|
|
137
|
+
currentSession.messages = messages
|
|
138
|
+
|
|
139
|
+
console.log('Chat initiated successfully')
|
|
140
|
+
|
|
141
|
+
return {
|
|
142
|
+
sessionId: currentSession.sessionId,
|
|
143
|
+
messages,
|
|
144
|
+
configData
|
|
145
|
+
}
|
|
146
|
+
} catch (error) {
|
|
147
|
+
console.error(`Failed to start chat: ${error.message}`)
|
|
148
|
+
cleanup()
|
|
149
|
+
throw error
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Disconnect from the current chat session
|
|
155
|
+
*/
|
|
156
|
+
export function disconnect() {
|
|
157
|
+
cleanup()
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Clean up the current session
|
|
162
|
+
*/
|
|
163
|
+
function cleanup() {
|
|
164
|
+
if (currentSession.abortController) {
|
|
165
|
+
currentSession.abortController.abort()
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const { callbacks, credentials } = currentSession
|
|
169
|
+
currentSession = createSession(callbacks)
|
|
170
|
+
currentSession.credentials = credentials
|
|
171
|
+
|
|
172
|
+
console.log('Chat session cleaned up')
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export function getExternalId() {
|
|
176
|
+
if (currentSession.credentials?.externalId) {
|
|
177
|
+
return currentSession.credentials.externalId
|
|
178
|
+
}
|
|
179
|
+
return getDeviceId()
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Send a message in the current chat session
|
|
184
|
+
* @param {{ text: string, html?: string }} message
|
|
185
|
+
* @returns {Promise<string>}
|
|
186
|
+
*/
|
|
187
|
+
export function sendMessage({ text, html }) {
|
|
188
|
+
return new Promise((resolve, reject) => {
|
|
189
|
+
;(async () => {
|
|
190
|
+
try {
|
|
191
|
+
// Add user message
|
|
192
|
+
const userMessage = {
|
|
193
|
+
role: MESSAGE_ROLES.USER,
|
|
194
|
+
text,
|
|
195
|
+
html,
|
|
196
|
+
timestamp: new Date().toISOString()
|
|
197
|
+
}
|
|
198
|
+
currentSession.messages = [...currentSession.messages, userMessage]
|
|
199
|
+
currentSession.callbacks.onMessagesUpdate?.([...currentSession.messages])
|
|
200
|
+
|
|
201
|
+
await sleep(200)
|
|
202
|
+
|
|
203
|
+
// Add loading message for bot if not in live agent mode
|
|
204
|
+
if (!currentSession.liveAgent) {
|
|
205
|
+
const loadingMessage = {
|
|
206
|
+
role: MESSAGE_ROLES.BOT,
|
|
207
|
+
text: '',
|
|
208
|
+
loading: true
|
|
209
|
+
}
|
|
210
|
+
currentSession.messages = [...currentSession.messages, loadingMessage]
|
|
211
|
+
currentSession.callbacks.onMessagesUpdate?.([...currentSession.messages])
|
|
212
|
+
} else {
|
|
213
|
+
resolve(currentSession.sessionId)
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const url = new URL(currentSession.sseUrl)
|
|
217
|
+
if (currentSession.sessionId) {
|
|
218
|
+
url.searchParams.set('sessionId', currentSession.sessionId)
|
|
219
|
+
}
|
|
220
|
+
if (currentSession.requestId) {
|
|
221
|
+
url.searchParams.set('requestId', currentSession.requestId)
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
currentSession.lastStreamId = undefined
|
|
225
|
+
|
|
226
|
+
// Create a new abort controller for this request
|
|
227
|
+
currentSession.abortController = new AbortController()
|
|
228
|
+
|
|
229
|
+
const headers = {
|
|
230
|
+
'Content-Type': 'application/json'
|
|
231
|
+
}
|
|
232
|
+
if (currentSession.credentials?.token) {
|
|
233
|
+
headers.Authorization = `Bearer ${currentSession.credentials.token}`
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
await fetchEventSource(url.toString(), {
|
|
237
|
+
method: 'POST',
|
|
238
|
+
headers,
|
|
239
|
+
body: JSON.stringify({
|
|
240
|
+
message: text,
|
|
241
|
+
html
|
|
242
|
+
}),
|
|
243
|
+
signal: currentSession.abortController.signal,
|
|
244
|
+
onopen: async (response) => {
|
|
245
|
+
if (!response.ok) {
|
|
246
|
+
console.error('Failed to send message bad response: ', response)
|
|
247
|
+
throw new Error('Failed to send message')
|
|
248
|
+
}
|
|
249
|
+
},
|
|
250
|
+
onmessage: (event) => {
|
|
251
|
+
console.log('Event: ', event)
|
|
252
|
+
const data = JSON.parse(event.data)
|
|
253
|
+
|
|
254
|
+
if (data.status === 'connected') {
|
|
255
|
+
currentSession.sessionId = data.sessionId
|
|
256
|
+
currentSession.requestId = data.requestId
|
|
257
|
+
if (currentSession.liveAgent) {
|
|
258
|
+
resolve(currentSession.sessionId)
|
|
259
|
+
}
|
|
260
|
+
} else if (data.agent) {
|
|
261
|
+
currentSession.liveAgent = true
|
|
262
|
+
const { type, data: payload } = data.agent
|
|
263
|
+
|
|
264
|
+
switch (type) {
|
|
265
|
+
case 'typing':
|
|
266
|
+
currentSession.callbacks.onTyping?.(true)
|
|
267
|
+
currentSession.callbacks.onLiveAgentMode?.(true)
|
|
268
|
+
break
|
|
269
|
+
case 'typingOff':
|
|
270
|
+
currentSession.callbacks.onTyping?.(false)
|
|
271
|
+
currentSession.callbacks.onLiveAgentMode?.(true)
|
|
272
|
+
break
|
|
273
|
+
case 'message':
|
|
274
|
+
if (payload.role !== MESSAGE_ROLES.USER) {
|
|
275
|
+
// Remove loading messages and add new message
|
|
276
|
+
currentSession.messages = currentSession.messages.filter((msg) => !msg.loading)
|
|
277
|
+
currentSession.messages = [...currentSession.messages, payload]
|
|
278
|
+
|
|
279
|
+
const isEnded = payload.action === 'ended' || payload.action === 'left'
|
|
280
|
+
if (isEnded) {
|
|
281
|
+
currentSession.liveAgent = false
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
currentSession.callbacks.onMessagesUpdate?.([...currentSession.messages])
|
|
285
|
+
currentSession.callbacks.onLiveAgentMode?.(!isEnded)
|
|
286
|
+
}
|
|
287
|
+
if (payload.action === 'ended' || payload.action === 'left') {
|
|
288
|
+
currentSession.liveAgent = false
|
|
289
|
+
}
|
|
290
|
+
break
|
|
291
|
+
default:
|
|
292
|
+
break
|
|
293
|
+
}
|
|
294
|
+
} else if (data.message !== undefined) {
|
|
295
|
+
let messages = currentSession.messages
|
|
296
|
+
|
|
297
|
+
// If streamId changes, start a new assistant message
|
|
298
|
+
if (data.streamId !== undefined) {
|
|
299
|
+
if (currentSession.lastStreamId === undefined) {
|
|
300
|
+
currentSession.lastStreamId = data.streamId
|
|
301
|
+
} else if (data.streamId !== currentSession.lastStreamId) {
|
|
302
|
+
currentSession.lastStreamId = data.streamId
|
|
303
|
+
messages = [
|
|
304
|
+
...messages,
|
|
305
|
+
{
|
|
306
|
+
role: MESSAGE_ROLES.BOT,
|
|
307
|
+
text: '',
|
|
308
|
+
loading: true
|
|
309
|
+
}
|
|
310
|
+
]
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// Update the last message with new content
|
|
315
|
+
currentSession.messages = messages.map((msg, index) => {
|
|
316
|
+
if (index === messages.length - 1) {
|
|
317
|
+
return {
|
|
318
|
+
...msg,
|
|
319
|
+
loading: false,
|
|
320
|
+
text: (msg.text || '') + data.message,
|
|
321
|
+
done: data.done ?? msg.done
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
return msg
|
|
325
|
+
})
|
|
326
|
+
|
|
327
|
+
currentSession.callbacks.onMessagesUpdate?.([...currentSession.messages])
|
|
328
|
+
|
|
329
|
+
if (data.done) {
|
|
330
|
+
resolve(currentSession.sessionId)
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// Store session info for reuse
|
|
334
|
+
currentSession.sessionId = data.session_id ?? currentSession.sessionId
|
|
335
|
+
currentSession.requestId = data.requestId ?? currentSession.requestId
|
|
336
|
+
} else if (data.error) {
|
|
337
|
+
const errorMessage = 'Failed to connect to the system'
|
|
338
|
+
currentSession.messages = currentSession.messages.map((msg, index) => {
|
|
339
|
+
if (index === currentSession.messages.length - 1) {
|
|
340
|
+
return {
|
|
341
|
+
...msg,
|
|
342
|
+
loading: false,
|
|
343
|
+
errorText: errorMessage
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
return msg
|
|
347
|
+
})
|
|
348
|
+
currentSession.callbacks.onMessagesUpdate?.([...currentSession.messages])
|
|
349
|
+
reject(new Error(errorMessage))
|
|
350
|
+
}
|
|
351
|
+
},
|
|
352
|
+
onerror: (error) => {
|
|
353
|
+
throw error // Rethrow to stop retries
|
|
354
|
+
},
|
|
355
|
+
openWhenHidden: true
|
|
356
|
+
})
|
|
357
|
+
} catch (error) {
|
|
358
|
+
console.error('Failed to send message: ', error)
|
|
359
|
+
const errorMessage = 'Failed to connect to the system'
|
|
360
|
+
currentSession.messages = currentSession.messages.map((msg, index) => {
|
|
361
|
+
if (index === currentSession.messages.length - 1) {
|
|
362
|
+
return {
|
|
363
|
+
...msg,
|
|
364
|
+
loading: false,
|
|
365
|
+
errorText: errorMessage,
|
|
366
|
+
done: true
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
return msg
|
|
370
|
+
})
|
|
371
|
+
currentSession.callbacks.onMessagesUpdate?.([...currentSession.messages])
|
|
372
|
+
reject(error)
|
|
373
|
+
}
|
|
374
|
+
})()
|
|
375
|
+
})
|
|
376
|
+
}
|
package/src/constants.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Constants for the Chat SDK
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export const MESSAGE_ROLES = {
|
|
6
|
+
BOT: 'assistant', // this can be automated or LLM AI Agent response
|
|
7
|
+
USER: 'user', // this is widget user
|
|
8
|
+
AGENT: 'agent', // this is human agent (dock side)
|
|
9
|
+
SYSTEM: 'system' // this is system message, for ex "Agent joined" / "Agent left"
|
|
10
|
+
}
|
package/src/http.js
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* API Service for Chat SDK
|
|
3
|
+
* Handles all HTTP requests without depending on external state
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { getExternalId } from './chat.js'
|
|
7
|
+
|
|
8
|
+
const AUTHENTICATION_ERROR = 'Something went wrong initializing the chat'
|
|
9
|
+
const INITIALIZATION_ERROR = 'Chat SDK not initialized'
|
|
10
|
+
|
|
11
|
+
// Module-level configuration
|
|
12
|
+
let _config = {
|
|
13
|
+
endpoint: null
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Configure the API service with endpoint
|
|
18
|
+
* @param {{ endpoint: string }} credentials
|
|
19
|
+
*/
|
|
20
|
+
export function configure(credentials) {
|
|
21
|
+
_config = {
|
|
22
|
+
endpoint: credentials.endpoint
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Get current configuration
|
|
28
|
+
* @returns {{ endpoint: string | null }}
|
|
29
|
+
*/
|
|
30
|
+
export function getConfig() {
|
|
31
|
+
return { ..._config }
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Authenticate with the chat service
|
|
36
|
+
* @param {{ endpoint: string }} credentials
|
|
37
|
+
* @returns {Promise<object>} Authentication response data
|
|
38
|
+
*/
|
|
39
|
+
export async function authenticate(payload) {
|
|
40
|
+
const { endpoint } = payload
|
|
41
|
+
const url = `${endpoint}/config`
|
|
42
|
+
|
|
43
|
+
const response = await fetch(url, {
|
|
44
|
+
method: 'GET',
|
|
45
|
+
headers: {
|
|
46
|
+
'Content-Type': 'application/json'
|
|
47
|
+
}
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
if (!response.ok) {
|
|
51
|
+
const errorPayload = await response.json()
|
|
52
|
+
throw new Error(errorPayload?.error || AUTHENTICATION_ERROR)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const res = await response.json()
|
|
56
|
+
const data = res.data
|
|
57
|
+
|
|
58
|
+
// Store endpoint for subsequent requests
|
|
59
|
+
configure({ endpoint })
|
|
60
|
+
|
|
61
|
+
return data
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Get chat history for the current device
|
|
66
|
+
* @returns {Promise<{ sessions: Array }>}
|
|
67
|
+
*/
|
|
68
|
+
export async function getHistory() {
|
|
69
|
+
const queryParams = new URLSearchParams({
|
|
70
|
+
externalId: getExternalId()
|
|
71
|
+
})
|
|
72
|
+
const response = await fetchRequest(`/sessions?${queryParams.toString()}`, 'GET')
|
|
73
|
+
|
|
74
|
+
if (!response.ok) {
|
|
75
|
+
throw new Error('Unable to load history, please try again later')
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return response.json()
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Get messages for a specific session
|
|
83
|
+
* @param {string} sessionId
|
|
84
|
+
* @returns {Promise<{ sessionHistory: Array }>}
|
|
85
|
+
*/
|
|
86
|
+
export async function getMessages(sessionId) {
|
|
87
|
+
const queryParams = new URLSearchParams({
|
|
88
|
+
sessionId
|
|
89
|
+
})
|
|
90
|
+
const response = await fetchRequest(`/session?${queryParams.toString()}`, 'GET')
|
|
91
|
+
|
|
92
|
+
if (!response.ok) {
|
|
93
|
+
throw new Error('Unable to load messages, please try again later')
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return response.json()
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Internal fetch request helper
|
|
101
|
+
* @param {string} pathname
|
|
102
|
+
* @param {string} method
|
|
103
|
+
* @param {object|null} body
|
|
104
|
+
* @returns {Promise<Response>}
|
|
105
|
+
*/
|
|
106
|
+
async function fetchRequest(pathname, method = 'GET', body = null) {
|
|
107
|
+
const { endpoint } = _config
|
|
108
|
+
|
|
109
|
+
if (!endpoint) {
|
|
110
|
+
throw new Error(INITIALIZATION_ERROR)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const url = `${endpoint}${pathname}`
|
|
114
|
+
|
|
115
|
+
return fetch(url, {
|
|
116
|
+
headers: {
|
|
117
|
+
'Content-Type': 'application/json'
|
|
118
|
+
},
|
|
119
|
+
method,
|
|
120
|
+
body: body ? JSON.stringify(body) : null
|
|
121
|
+
})
|
|
122
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @origon/chat-sdk
|
|
3
|
+
*
|
|
4
|
+
* Chat SDK for Origon/Samespace - provides core chat functionality
|
|
5
|
+
* without UI dependencies.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* ```js
|
|
9
|
+
* import {
|
|
10
|
+
* authenticate,
|
|
11
|
+
* initiate,
|
|
12
|
+
* sendMessage,
|
|
13
|
+
* disconnect,
|
|
14
|
+
* getHistory,
|
|
15
|
+
* setCallbacks
|
|
16
|
+
* } from '@origon/chat-sdk'
|
|
17
|
+
*
|
|
18
|
+
* // Authenticate first
|
|
19
|
+
* const config = await authenticate({ endpoint: '...', apiKey: '...' })
|
|
20
|
+
*
|
|
21
|
+
* // Set up callbacks for state updates
|
|
22
|
+
* setCallbacks({
|
|
23
|
+
* onMessage: (msg) => console.log('New message:', msg),
|
|
24
|
+
* onMessagesUpdate: (messages) => updateUI(messages),
|
|
25
|
+
* onTyping: (isTyping) => showTypingIndicator(isTyping),
|
|
26
|
+
* onError: (error) => console.error(error)
|
|
27
|
+
* })
|
|
28
|
+
*
|
|
29
|
+
* // Start a chat session
|
|
30
|
+
* const { sessionId, messages } = await initiate()
|
|
31
|
+
*
|
|
32
|
+
* // Send messages
|
|
33
|
+
* await sendMessage({ text: 'Hello!' })
|
|
34
|
+
*
|
|
35
|
+
* // Disconnect when done
|
|
36
|
+
* disconnect()
|
|
37
|
+
* ```
|
|
38
|
+
*/
|
|
39
|
+
|
|
40
|
+
// HTTP API functions
|
|
41
|
+
export { authenticate, getHistory, getMessages, configure } from './http.js'
|
|
42
|
+
|
|
43
|
+
// Chat functions
|
|
44
|
+
export { initialize, startChat, sendMessage, disconnect, setCallbacks } from './chat.js'
|
|
45
|
+
|
|
46
|
+
// Call functions
|
|
47
|
+
export {
|
|
48
|
+
startCall,
|
|
49
|
+
disconnectCall,
|
|
50
|
+
toggleMute,
|
|
51
|
+
getLocalStream,
|
|
52
|
+
getInboundAudioEnergy,
|
|
53
|
+
getOutboundAudioEnergy,
|
|
54
|
+
setCallCallbacks
|
|
55
|
+
} from './call.js'
|
|
56
|
+
|
|
57
|
+
// Constants
|
|
58
|
+
export { MESSAGE_ROLES } from './constants.js'
|
package/src/utils.js
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utility functions for the Chat SDK
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export function uuidv7() {
|
|
6
|
+
const timestamp = Date.now()
|
|
7
|
+
const bytes = new Uint8Array(16)
|
|
8
|
+
crypto.getRandomValues(bytes)
|
|
9
|
+
|
|
10
|
+
// Set timestamp (48 bits)
|
|
11
|
+
bytes[0] = (timestamp >> 40) & 0xff
|
|
12
|
+
bytes[1] = (timestamp >> 32) & 0xff
|
|
13
|
+
bytes[2] = (timestamp >> 24) & 0xff
|
|
14
|
+
bytes[3] = (timestamp >> 16) & 0xff
|
|
15
|
+
bytes[4] = (timestamp >> 8) & 0xff
|
|
16
|
+
bytes[5] = timestamp & 0xff
|
|
17
|
+
|
|
18
|
+
// Set version 7 (0111)
|
|
19
|
+
bytes[6] = (bytes[6] & 0x0f) | 0x70
|
|
20
|
+
|
|
21
|
+
// Set variant (10xx)
|
|
22
|
+
bytes[8] = (bytes[8] & 0x3f) | 0x80
|
|
23
|
+
|
|
24
|
+
const hex = [...bytes].map((b) => b.toString(16).padStart(2, '0')).join('')
|
|
25
|
+
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(
|
|
26
|
+
16,
|
|
27
|
+
20
|
|
28
|
+
)}-${hex.slice(20)}`
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function getDeviceId() {
|
|
32
|
+
if (localStorage.getItem('chatDeviceId')) {
|
|
33
|
+
return localStorage.getItem('chatDeviceId')
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const deviceId = uuidv7()
|
|
37
|
+
localStorage.setItem('chatDeviceId', deviceId)
|
|
38
|
+
return deviceId
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function parseJwt(token) {
|
|
42
|
+
try {
|
|
43
|
+
const base64Url = token.split('.')[1]
|
|
44
|
+
const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/')
|
|
45
|
+
const jsonPayload = decodeURIComponent(
|
|
46
|
+
atob(base64)
|
|
47
|
+
.split('')
|
|
48
|
+
.map(function (c) {
|
|
49
|
+
return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2)
|
|
50
|
+
})
|
|
51
|
+
.join('')
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
return JSON.parse(jsonPayload)
|
|
55
|
+
} catch {
|
|
56
|
+
return null
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export async function sleep(ms) {
|
|
61
|
+
return new Promise((resolve) => setTimeout(resolve, ms))
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function getSocketEndpoint(baseUrl) {
|
|
65
|
+
let socketEndpoint
|
|
66
|
+
try {
|
|
67
|
+
const url = new URL(baseUrl)
|
|
68
|
+
socketEndpoint = `wss://${url.hostname}${url.pathname}/wss`
|
|
69
|
+
} catch {
|
|
70
|
+
console.error('Invalid base URL: ', baseUrl)
|
|
71
|
+
}
|
|
72
|
+
return socketEndpoint
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function getCallServerEndpoint(baseUrl) {
|
|
76
|
+
let socketEndpoint
|
|
77
|
+
try {
|
|
78
|
+
const url = new URL(baseUrl)
|
|
79
|
+
socketEndpoint = `wss://${url.hostname}${url.pathname}/audio`
|
|
80
|
+
} catch {
|
|
81
|
+
console.error('getCallServerEndpoint: Invalid base URL: ', baseUrl)
|
|
82
|
+
}
|
|
83
|
+
return socketEndpoint
|
|
84
|
+
}
|