@ourlu/assistant-sdk 0.2.4 → 0.2.5
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/iife/audio.v1.028c93fe.js +160 -0
- package/dist/iife/audio.v1.051ececf.js +183 -0
- package/dist/iife/audio.v1.200db1a6.js +208 -0
- package/dist/iife/audio.v1.20858b08.js +191 -0
- package/dist/iife/audio.v1.2690e445.js +179 -0
- package/dist/iife/audio.v1.2742ff12.js +188 -0
- package/dist/iife/audio.v1.70af81b3.js +191 -0
- package/dist/iife/audio.v1.95146620.js +1 -1
- package/dist/iife/audio.v1.b330baaf.js +166 -0
- package/dist/iife/audio.v1.bb9c2d88.js +181 -0
- package/dist/iife/audio.v1.ea7571b2.js +182 -0
- package/dist/iife/audio.v1.fc0aa8af.js +191 -0
- package/dist/iife/audio.v1.js +107 -95
- package/dist/iife/engine.v1.2b5bb43b.js +3 -3
- package/dist/iife/engine.v1.3b09dc20.js +3 -3
- package/dist/iife/engine.v1.56074e5a.js +769 -0
- package/dist/iife/engine.v1.61c10e6c.js +770 -0
- package/dist/iife/engine.v1.773fc15d.js +2 -2
- package/dist/iife/engine.v1.80d2230f.js +3 -3
- package/dist/iife/engine.v1.940ba9ea.js +764 -0
- package/dist/iife/engine.v1.99a33ee2.js +767 -0
- package/dist/iife/engine.v1.9ca6b7ec.js +3 -3
- package/dist/iife/engine.v1.a1f7dea2.js +764 -0
- package/dist/iife/engine.v1.c0c00bd0.js +3 -3
- package/dist/iife/engine.v1.c127656e.js +820 -0
- package/dist/iife/engine.v1.c54c9a1a.js +3 -3
- package/dist/iife/engine.v1.d1052e81.js +3 -3
- package/dist/iife/engine.v1.f6d23a0f.js +770 -0
- package/dist/iife/engine.v1.js +75 -25
- package/dist/iife/loader.v1.js +5 -1
- package/dist/iife/signalement.v1.d321dfde.js +518 -0
- package/dist/iife/signalement.v1.js +518 -0
- package/dist/iife/ui.v1.00abf020.js +895 -0
- package/dist/iife/ui.v1.6becaa84.js +895 -0
- package/dist/iife/ui.v1.6c9e4995.js +895 -0
- package/dist/iife/ui.v1.88bf5494.js +898 -0
- package/dist/iife/ui.v1.a8cfe724.js +900 -0
- package/dist/iife/ui.v1.e007c7c4.js +926 -0
- package/dist/iife/ui.v1.e24ba2bd.js +903 -0
- package/dist/iife/ui.v1.f1d8e998.js +903 -0
- package/dist/iife/ui.v1.fc52b520.js +895 -0
- package/dist/iife/ui.v1.js +44 -77
- package/dist/iife/widget-manifest.json +4 -3
- package/package.json +2 -1
|
@@ -0,0 +1,767 @@
|
|
|
1
|
+
(function() {
|
|
2
|
+
"use strict";
|
|
3
|
+
var runtime = window.__OurluWidgetRuntimeV1 || (window.__OurluWidgetRuntimeV1 = {});
|
|
4
|
+
if (!runtime.utils || !runtime.WidgetUIManager || !runtime.WidgetAudioManager || !runtime.EventBus) {
|
|
5
|
+
throw new Error("Widget runtime UI module must be loaded first.");
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
var WIDGET_VERSION = "v1", WIDGET_NS = "OurluMairieWidget";
|
|
9
|
+
var trimTrailingSlash = runtime.utils.trimTrailingSlash;
|
|
10
|
+
var WidgetUIManager = runtime.WidgetUIManager, WidgetAudioManager = runtime.WidgetAudioManager;
|
|
11
|
+
var EventBus = runtime.EventBus, CONTAINER_ID = runtime.constants.CONTAINER_ID;
|
|
12
|
+
var MASCOT_SPECS = JSON.parse('[["mascotSecondaryColor","mascot_secondary_color","data-mascot-secondary-color","#68b1d6"],["mascotSecondaryDarkColor","mascot_secondary_dark_color","data-mascot-secondary-dark-color","#1472a8"],["mascotGoldColor","mascot_gold_color","data-mascot-gold-color","#ffd22e"],["mascotEyeColor","mascot_eye_color","data-mascot-eye-color","#040402"],["mascotBeakColor","mascot_beak_color","data-mascot-beak-color","#ab5f30"],["mascotClawColor","mascot_claw_color","data-mascot-claw-color","#ab5f30"],["mascotNeutralColor","mascot_neutral_color","data-mascot-neutral-color","#e6e6e6"],["mascotBrowColor","mascot_brow_color","data-mascot-brow-color","#3e3e3e"]]');
|
|
13
|
+
var CONFIG_STRING_ATTRS = JSON.parse('[["tenantId","data-tenant-id",""],["widgetKey","data-widget-key",""],["apiBaseUrl","data-api-base-url",""],["widgetOrigin","data-widget-origin",""],["bootstrapEndpoint","data-bootstrap-endpoint",""],["configEndpoint","data-config-endpoint",""],["sessionBootstrapToken","data-session-bootstrap-token",""],["sessionBootstrapTokenExpiresAt","data-session-bootstrap-token-expires-at",""],["chatSessionEndpoint","data-chat-session-endpoint","/v1/occe/chat/sessions"],["chatMessageTemplate","data-chat-message-template","/v1/occe/chat/sessions/{session_id}/messages"],["chatStreamTemplate","data-chat-stream-template","/v1/occe/chat/sessions/{session_id}/stream"],["chatAudioSessionTemplate","data-chat-audio-session-template","/v1/occe/chat/sessions/{session_id}/audio/session"],["chatAudioChunkTemplate","data-chat-audio-chunk-template","/v1/occe/chat/sessions/{session_id}/audio/chunk"],["chatAudioCloseTemplate","data-chat-audio-close-template","/v1/occe/chat/sessions/{session_id}/audio/close"],["chatAudioStreamTemplate","data-chat-audio-stream-template","/v1/occe/chat/sessions/{session_id}/audio/stream"],["panelWidth","data-panel-width","420"],["panelHeight","data-panel-height","640"],["panelMaxHeightVh","data-panel-max-height-vh","85"],["mascotUrl","data-mascot-url",""],["welcomeMessage","data-welcome-message","Bonjour ! Comment puis-je vous aider ?"],["disclaimerText","data-disclaimer-text","Les r\u00e9ponses g\u00e9n\u00e9r\u00e9es par IA ne constituent pas une d\u00e9cision administrative officielle."],["transparencyText","data-transparency-text","Propuls\u00e9 par Ourlu \u00b7 IA locale et transparente."]]');
|
|
14
|
+
var RUNTIME_ENDPOINT_SPECS = JSON.parse('[["chatSessionEndpoint","chat_session_endpoint","/v1/occe/chat/sessions"],["chatMessageTemplate","chat_message_template","/v1/occe/chat/sessions/{session_id}/messages"],["chatStreamTemplate","chat_stream_template","/v1/occe/chat/sessions/{session_id}/stream"],["chatAudioSessionTemplate","chat_audio_session_template","/v1/occe/chat/sessions/{session_id}/audio/session"],["chatAudioChunkTemplate","chat_audio_chunk_template","/v1/occe/chat/sessions/{session_id}/audio/chunk"],["chatAudioCloseTemplate","chat_audio_close_template","/v1/occe/chat/sessions/{session_id}/audio/close"],["chatAudioStreamTemplate","chat_audio_stream_template","/v1/occe/chat/sessions/{session_id}/audio/stream"]]');
|
|
15
|
+
function applyMascotLabelColors(config, labels) {
|
|
16
|
+
MASCOT_SPECS.forEach(function(spec) {
|
|
17
|
+
config[spec[0]] = normalizeHexColor(labels[spec[1]], config[spec[0]] || spec[3]);
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
function applyScriptMascotColors(config, scriptTag) {
|
|
21
|
+
MASCOT_SPECS.forEach(function(spec) {
|
|
22
|
+
config[spec[0]] = readScriptHexAttr(scriptTag, spec[2], spec[3]);
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
function readScriptAttr(scriptTag, attributeName, fallbackValue) {
|
|
26
|
+
var value = scriptTag.getAttribute(attributeName);
|
|
27
|
+
return value == null || value === "" ? fallbackValue : value;
|
|
28
|
+
}
|
|
29
|
+
function readScriptHexAttr(scriptTag, attributeName, fallbackColor) {
|
|
30
|
+
return normalizeHexColor(readScriptAttr(scriptTag, attributeName, ""), fallbackColor);
|
|
31
|
+
}
|
|
32
|
+
function assignRuntimeEndpoints(config, runtimeConfig) {
|
|
33
|
+
RUNTIME_ENDPOINT_SPECS.forEach(function(spec) {
|
|
34
|
+
config[spec[0]] = String(runtimeConfig[spec[1]] || config[spec[0]] || spec[2]).trim();
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
function resolveAbsoluteMascotUrl(mascotUrl, apiBaseUrl) {
|
|
38
|
+
if (runtime.utils && typeof runtime.utils.resolveAbsoluteMascotUrl === "function") {
|
|
39
|
+
return runtime.utils.resolveAbsoluteMascotUrl(mascotUrl, apiBaseUrl);
|
|
40
|
+
}
|
|
41
|
+
var normalized = String(mascotUrl || "").trim();
|
|
42
|
+
if (!normalized) return "";
|
|
43
|
+
if (/^https?:\/\//i.test(normalized)) return normalized;
|
|
44
|
+
var base = String(apiBaseUrl || "").trim().replace(/\/$/, "");
|
|
45
|
+
if (!base) {
|
|
46
|
+
throw new Error("[OurluMairie] mascot URL relative sans apiBaseUrl: " + normalized);
|
|
47
|
+
}
|
|
48
|
+
return normalized.startsWith("/") ? base + normalized : base + "/" + normalized;
|
|
49
|
+
}
|
|
50
|
+
function normalizeHexColor(rawValue, fallbackColor) {
|
|
51
|
+
var normalized = String(rawValue || "").trim().toLowerCase();
|
|
52
|
+
return /^#[0-9a-f]{6}$/.test(normalized) ? normalized : fallbackColor;
|
|
53
|
+
}
|
|
54
|
+
function normalizePosition(rawValue, fallbackValue) {
|
|
55
|
+
var normalized = String(rawValue || "").trim().toLowerCase();
|
|
56
|
+
return normalized === "bottom-left" || normalized === "bottom-right" ? normalized : fallbackValue;
|
|
57
|
+
}
|
|
58
|
+
function normalizeThemeVersion(rawValue) {
|
|
59
|
+
var parsed = Number(rawValue);
|
|
60
|
+
return !Number.isFinite(parsed) || parsed <= 0 ? 0 : Math.floor(parsed);
|
|
61
|
+
}
|
|
62
|
+
function normalizeThemeModeStrategy(rawValue) {
|
|
63
|
+
var normalized = String(rawValue || "").trim().toLowerCase();
|
|
64
|
+
return normalized === "manual_dark" || normalized === "auto_follow_host" || normalized === "manual_light" ? normalized : "manual_light";
|
|
65
|
+
}
|
|
66
|
+
function resolveHostPrefersDark(fallbackValue) {
|
|
67
|
+
if (typeof window === "undefined" || typeof window.matchMedia !== "function") return Boolean(fallbackValue);
|
|
68
|
+
return Boolean(window.matchMedia("(prefers-color-scheme: dark)").matches);
|
|
69
|
+
}
|
|
70
|
+
function resolvePreferredThemeScheme(config) {
|
|
71
|
+
var mode = normalizeThemeModeStrategy((config || {}).themeModeStrategy);
|
|
72
|
+
if (mode === "manual_dark") return "dark";
|
|
73
|
+
if (mode === "auto_follow_host") return resolveHostPrefersDark((config || {}).hostPrefersDark) ? "dark" : "light";
|
|
74
|
+
return "light";
|
|
75
|
+
}
|
|
76
|
+
function clampNumericString(rawValue, fallbackValue, minValue, maxValue, shouldRound) {
|
|
77
|
+
var parsed = Number(rawValue);
|
|
78
|
+
if (!Number.isFinite(parsed)) return fallbackValue;
|
|
79
|
+
if (parsed < minValue) return String(minValue);
|
|
80
|
+
if (parsed > maxValue) return String(maxValue);
|
|
81
|
+
return shouldRound ? String(Math.round(parsed)) : String(parsed);
|
|
82
|
+
}
|
|
83
|
+
function decodeBase64UrlPayload(rawValue) {
|
|
84
|
+
var encoded = String(rawValue || "").trim();
|
|
85
|
+
if (!encoded) return "";
|
|
86
|
+
var base64 = encoded.replace(/-/g, "+").replace(/_/g, "/");
|
|
87
|
+
while (base64.length % 4 !== 0) base64 += "=";
|
|
88
|
+
var binary = atob(base64);
|
|
89
|
+
var percentEncoded = "";
|
|
90
|
+
for (var index = 0; index < binary.length; index += 1) {
|
|
91
|
+
percentEncoded += "%" + ("00" + binary.charCodeAt(index).toString(16)).slice(-2);
|
|
92
|
+
}
|
|
93
|
+
return decodeURIComponent(percentEncoded);
|
|
94
|
+
}
|
|
95
|
+
function parseThemeSnapshot(rawValue) {
|
|
96
|
+
try {
|
|
97
|
+
var decodedPayload = decodeBase64UrlPayload(rawValue);
|
|
98
|
+
if (!decodedPayload) return null;
|
|
99
|
+
var parsedPayload = JSON.parse(decodedPayload);
|
|
100
|
+
return parsedPayload && typeof parsedPayload === "object" ? parsedPayload : null;
|
|
101
|
+
} catch (_) {
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
function applyThemeConfigToRuntime(config, rawWidgetConfig) {
|
|
106
|
+
if (!rawWidgetConfig || typeof rawWidgetConfig !== "object") return false;
|
|
107
|
+
var labels = rawWidgetConfig.labels && typeof rawWidgetConfig.labels === "object" ? rawWidgetConfig.labels : {};
|
|
108
|
+
config.themeSplitEnabled = Boolean(rawWidgetConfig.theme_split_enabled);
|
|
109
|
+
config.themeModeStrategy = normalizeThemeModeStrategy(rawWidgetConfig.theme_mode_strategy || labels.theme_mode_strategy || config.themeModeStrategy);
|
|
110
|
+
config.hostPrefersDark = resolveHostPrefersDark(config.hostPrefersDark);
|
|
111
|
+
config.effectiveThemeScheme = resolvePreferredThemeScheme(config);
|
|
112
|
+
var resolvedThemeConfig = rawWidgetConfig;
|
|
113
|
+
if (config.themeSplitEnabled) {
|
|
114
|
+
var splitConfig = config.effectiveThemeScheme === "dark" ? rawWidgetConfig.dark_theme_defaults : rawWidgetConfig.light_theme_defaults;
|
|
115
|
+
if (splitConfig && typeof splitConfig === "object") {
|
|
116
|
+
var splitLabels = splitConfig.labels && typeof splitConfig.labels === "object" ? splitConfig.labels : {};
|
|
117
|
+
labels = Object.assign({}, labels, splitLabels);
|
|
118
|
+
resolvedThemeConfig = { primary_color: splitConfig.primary_color || rawWidgetConfig.primary_color, position: splitConfig.position || rawWidgetConfig.position, labels: labels };
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
config.primaryColor = normalizeHexColor(resolvedThemeConfig.primary_color, config.primaryColor || "#0066ff");
|
|
122
|
+
config.position = normalizePosition(resolvedThemeConfig.position, config.position || "bottom-right");
|
|
123
|
+
var themedAssistantName = String(labels.title || labels.assistant_name || "").trim();
|
|
124
|
+
if (themedAssistantName) {
|
|
125
|
+
config.assistantName = themedAssistantName;
|
|
126
|
+
} else if (!String(config.assistantName || "").trim()) {
|
|
127
|
+
config.assistantName = "Assistant mairie";
|
|
128
|
+
}
|
|
129
|
+
config.panelBackgroundColor = normalizeHexColor(labels.panel_background_color, config.panelBackgroundColor || "#ffffff");
|
|
130
|
+
config.panelBackgroundAlpha = clampNumericString(labels.panel_background_alpha, config.panelBackgroundAlpha || "1", 0, 1, false);
|
|
131
|
+
config.panelBorderRadius = clampNumericString(labels.panel_border_radius, config.panelBorderRadius || "16", 0, 32, true);
|
|
132
|
+
applyMascotLabelColors(config, labels);
|
|
133
|
+
var candidateVersion = normalizeThemeVersion(rawWidgetConfig.theme_updated_at);
|
|
134
|
+
if (candidateVersion > 0) config.themeVersion = candidateVersion;
|
|
135
|
+
return true;
|
|
136
|
+
}
|
|
137
|
+
function applyWidgetTextConfig(config, widgetConfig) {
|
|
138
|
+
if (!widgetConfig || typeof widgetConfig !== "object") return;
|
|
139
|
+
var welcomeMessage = String(widgetConfig.welcome_message || "").trim();
|
|
140
|
+
if (welcomeMessage) config.welcomeMessage = welcomeMessage;
|
|
141
|
+
var disclaimerText = String(widgetConfig.disclaimer_text || "").trim();
|
|
142
|
+
if (disclaimerText) config.disclaimerText = disclaimerText;
|
|
143
|
+
var mascotUrl = String(widgetConfig.mascot_asset_svg_url || "").trim();
|
|
144
|
+
if (mascotUrl) config.mascotUrl = resolveAbsoluteMascotUrl(mascotUrl, config.apiBaseUrl);
|
|
145
|
+
var chatbotName = String(widgetConfig.chatbot_name || "").trim();
|
|
146
|
+
if (chatbotName) config.assistantName = chatbotName;
|
|
147
|
+
}
|
|
148
|
+
var TURNSTILE_SCRIPT_URL = "https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit";
|
|
149
|
+
var TURNSTILE_CONTAINER_ID = "__ourlu_turnstile_container";
|
|
150
|
+
var TURNSTILE_LOAD_TIMEOUT_MS = 15000;
|
|
151
|
+
var TURNSTILE_CHALLENGE_TIMEOUT_MS = 120000;
|
|
152
|
+
var TURNSTILE_TOKEN_TTL_MS = 280000;
|
|
153
|
+
function TurnstileChallengeManager(siteKey) {
|
|
154
|
+
this.siteKey = String(siteKey || "").trim();
|
|
155
|
+
this.scriptLoaded = false;
|
|
156
|
+
this.scriptLoadPromise = null;
|
|
157
|
+
this.widgetId = null;
|
|
158
|
+
this.cachedToken = "";
|
|
159
|
+
this.cachedTokenTimestamp = 0;
|
|
160
|
+
}
|
|
161
|
+
Object.assign(TurnstileChallengeManager.prototype, {
|
|
162
|
+
isEnabled: function() { return Boolean(this.siteKey); },
|
|
163
|
+
loadScript: function() {
|
|
164
|
+
if (this.scriptLoaded) return Promise.resolve();
|
|
165
|
+
if (this.scriptLoadPromise) return this.scriptLoadPromise;
|
|
166
|
+
var self = this;
|
|
167
|
+
this.scriptLoadPromise = new Promise(function(resolve, reject) {
|
|
168
|
+
if (typeof window.turnstile !== "undefined") { self.scriptLoaded = true; return resolve(); }
|
|
169
|
+
var script = document.createElement("script");
|
|
170
|
+
script.src = TURNSTILE_SCRIPT_URL;
|
|
171
|
+
script.async = true;
|
|
172
|
+
var timeoutId = setTimeout(function() {
|
|
173
|
+
reject(new Error("Le module de sécurité n'a pas pu se charger. Veuillez réessayer."));
|
|
174
|
+
}, TURNSTILE_LOAD_TIMEOUT_MS);
|
|
175
|
+
script.onload = function() { clearTimeout(timeoutId); self.scriptLoaded = true; resolve(); };
|
|
176
|
+
script.onerror = function() { clearTimeout(timeoutId); self.scriptLoadPromise = null; reject(new Error("Impossible de charger le module de sécurité.")); };
|
|
177
|
+
document.head.appendChild(script);
|
|
178
|
+
});
|
|
179
|
+
return this.scriptLoadPromise;
|
|
180
|
+
},
|
|
181
|
+
ensureContainer: function() {
|
|
182
|
+
var existing = document.getElementById(TURNSTILE_CONTAINER_ID);
|
|
183
|
+
if (existing) return existing;
|
|
184
|
+
var container = document.createElement("div");
|
|
185
|
+
container.id = TURNSTILE_CONTAINER_ID;
|
|
186
|
+
container.style.cssText = "display:none;position:fixed;z-index:2147483647;background:transparent;padding:0;";
|
|
187
|
+
document.body.appendChild(container);
|
|
188
|
+
return container;
|
|
189
|
+
},
|
|
190
|
+
positionInsidePanel: function(container) {
|
|
191
|
+
var widgetHost = document.getElementById("__ourlu_widget_container");
|
|
192
|
+
if (!widgetHost || !widgetHost.shadowRoot) {
|
|
193
|
+
container.style.right = "24px";
|
|
194
|
+
container.style.bottom = "90px";
|
|
195
|
+
container.style.left = "";
|
|
196
|
+
container.style.top = "";
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
var panel = widgetHost.shadowRoot.querySelector("#cm-panel");
|
|
200
|
+
if (!panel) {
|
|
201
|
+
container.style.right = "24px";
|
|
202
|
+
container.style.bottom = "90px";
|
|
203
|
+
container.style.left = "";
|
|
204
|
+
container.style.top = "";
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
var rect = panel.getBoundingClientRect();
|
|
208
|
+
if (rect.width === 0 || rect.height === 0) {
|
|
209
|
+
container.style.right = "24px";
|
|
210
|
+
container.style.bottom = "90px";
|
|
211
|
+
container.style.left = "";
|
|
212
|
+
container.style.top = "";
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
var containerWidth = 300;
|
|
216
|
+
var containerHeight = 65;
|
|
217
|
+
container.style.left = Math.round(rect.left + (rect.width - containerWidth) / 2) + "px";
|
|
218
|
+
container.style.top = Math.round(rect.top + rect.height - containerHeight - 70) + "px";
|
|
219
|
+
container.style.right = "";
|
|
220
|
+
container.style.bottom = "";
|
|
221
|
+
},
|
|
222
|
+
showContainer: function(container) {
|
|
223
|
+
container.style.display = "block";
|
|
224
|
+
this.positionInsidePanel(container);
|
|
225
|
+
},
|
|
226
|
+
hideContainer: function(container) { container.style.display = "none"; },
|
|
227
|
+
isCachedTokenValid: function() {
|
|
228
|
+
return Boolean(this.cachedToken) && (Date.now() - this.cachedTokenTimestamp) < TURNSTILE_TOKEN_TTL_MS;
|
|
229
|
+
},
|
|
230
|
+
execute: async function() {
|
|
231
|
+
if (!this.isEnabled()) return "";
|
|
232
|
+
if (this.isCachedTokenValid()) return this.cachedToken;
|
|
233
|
+
await this.loadScript();
|
|
234
|
+
if (typeof window.turnstile === "undefined") {
|
|
235
|
+
throw new Error("Le module de sécurité n'est pas disponible.");
|
|
236
|
+
}
|
|
237
|
+
var self = this;
|
|
238
|
+
var token = await new Promise(function(resolve, reject) {
|
|
239
|
+
var container = self.ensureContainer();
|
|
240
|
+
var timeoutId = setTimeout(function() {
|
|
241
|
+
self.hideContainer(container);
|
|
242
|
+
self.cleanup();
|
|
243
|
+
reject(new Error("Vérification de sécurité expirée. Veuillez réessayer."));
|
|
244
|
+
}, TURNSTILE_CHALLENGE_TIMEOUT_MS);
|
|
245
|
+
if (self.widgetId !== null) { window.turnstile.remove(self.widgetId); self.widgetId = null; }
|
|
246
|
+
self.widgetId = window.turnstile.render(container, {
|
|
247
|
+
sitekey: self.siteKey,
|
|
248
|
+
language: "fr",
|
|
249
|
+
appearance: "interaction-only",
|
|
250
|
+
"before-interactive-callback": function() {
|
|
251
|
+
self.showContainer(container);
|
|
252
|
+
},
|
|
253
|
+
callback: function(freshToken) {
|
|
254
|
+
clearTimeout(timeoutId);
|
|
255
|
+
self.hideContainer(container);
|
|
256
|
+
resolve(freshToken);
|
|
257
|
+
},
|
|
258
|
+
"error-callback": function(errorCode) {
|
|
259
|
+
clearTimeout(timeoutId);
|
|
260
|
+
self.hideContainer(container);
|
|
261
|
+
self.cleanup();
|
|
262
|
+
reject(new Error("Erreur lors de la vérification de sécurité. Veuillez réessayer."));
|
|
263
|
+
},
|
|
264
|
+
"timeout-callback": function() {
|
|
265
|
+
clearTimeout(timeoutId);
|
|
266
|
+
self.hideContainer(container);
|
|
267
|
+
self.cleanup();
|
|
268
|
+
reject(new Error("La vérification de sécurité a expiré. Veuillez réessayer."));
|
|
269
|
+
}
|
|
270
|
+
});
|
|
271
|
+
});
|
|
272
|
+
this.cachedToken = token;
|
|
273
|
+
this.cachedTokenTimestamp = Date.now();
|
|
274
|
+
return token;
|
|
275
|
+
},
|
|
276
|
+
invalidateCache: function() {
|
|
277
|
+
this.cachedToken = "";
|
|
278
|
+
this.cachedTokenTimestamp = 0;
|
|
279
|
+
},
|
|
280
|
+
cleanup: function() {
|
|
281
|
+
if (this.widgetId !== null && typeof window.turnstile !== "undefined") {
|
|
282
|
+
try { window.turnstile.remove(this.widgetId); } catch (_) {}
|
|
283
|
+
this.widgetId = null;
|
|
284
|
+
}
|
|
285
|
+
},
|
|
286
|
+
reset: function() {
|
|
287
|
+
if (this.widgetId !== null && typeof window.turnstile !== "undefined") {
|
|
288
|
+
try { window.turnstile.reset(this.widgetId); } catch (_) {}
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
});
|
|
292
|
+
function WidgetApiManager(config, state, events, uiManager, turnstile) {
|
|
293
|
+
this.config = config;
|
|
294
|
+
this.state = state;
|
|
295
|
+
this.events = events;
|
|
296
|
+
this.uiManager = uiManager;
|
|
297
|
+
this.turnstile = turnstile || null;
|
|
298
|
+
this.prefetchedBootstrapAcked = false;
|
|
299
|
+
}
|
|
300
|
+
Object.assign(WidgetApiManager.prototype, {
|
|
301
|
+
resolveRuntimeUrl: function(pathTemplate, sessionId) {
|
|
302
|
+
var template = String(pathTemplate || "").trim() || "/v1/occe/chat/sessions";
|
|
303
|
+
if (sessionId) template = template.replace("{session_id}", encodeURIComponent(String(sessionId)));
|
|
304
|
+
if (/^https?:\/\//i.test(template)) return template;
|
|
305
|
+
if (template.charAt(0) !== "/") template = "/" + template;
|
|
306
|
+
return this.config.apiBaseUrl + template;
|
|
307
|
+
},
|
|
308
|
+
resolveWidgetOrigin: function() {
|
|
309
|
+
if (typeof window !== "undefined" && window.location && window.location.origin) {
|
|
310
|
+
return String(window.location.origin).trim();
|
|
311
|
+
}
|
|
312
|
+
return String(this.config.widgetOrigin || "").trim();
|
|
313
|
+
},
|
|
314
|
+
applyRuntimeConfig: function(payload, sourceLabel) {
|
|
315
|
+
var runtimeConfig = payload && payload.runtime_config ? payload.runtime_config : {};
|
|
316
|
+
assignRuntimeEndpoints(this.config, runtimeConfig);
|
|
317
|
+
var incomingTurnstileKey = String((payload && payload.turnstile_site_key) || (runtimeConfig && runtimeConfig.turnstile_site_key) || "").trim();
|
|
318
|
+
if (incomingTurnstileKey && !this.config.turnstileSiteKey) {
|
|
319
|
+
this.config.turnstileSiteKey = incomingTurnstileKey;
|
|
320
|
+
if (!this.turnstile) this.turnstile = new TurnstileChallengeManager(incomingTurnstileKey);
|
|
321
|
+
}
|
|
322
|
+
var widgetConfig = payload && payload.widget_config ? payload.widget_config : null;
|
|
323
|
+
if (!widgetConfig) return;
|
|
324
|
+
var currentThemeVersion = normalizeThemeVersion(this.config.themeVersion);
|
|
325
|
+
var incomingThemeVersion = normalizeThemeVersion(widgetConfig.theme_updated_at);
|
|
326
|
+
if (this.config.hasSnapshotTheme && incomingThemeVersion > 0 && incomingThemeVersion < currentThemeVersion) return;
|
|
327
|
+
applyWidgetTextConfig(this.config, widgetConfig);
|
|
328
|
+
applyThemeConfigToRuntime(this.config, widgetConfig);
|
|
329
|
+
this.config.themeVersion = incomingThemeVersion > 0 ? incomingThemeVersion : currentThemeVersion;
|
|
330
|
+
this.config.hasSnapshotTheme = this.config.hasSnapshotTheme && this.config.themeVersion > 0;
|
|
331
|
+
if (this.uiManager && typeof this.uiManager.updateTheme === "function") this.uiManager.updateTheme(this.config);
|
|
332
|
+
this.events.emit("theme_updated", { source: String(sourceLabel || "runtime_config"), theme_version: this.config.themeVersion || 0 });
|
|
333
|
+
},
|
|
334
|
+
buildHeaders: function(extra) {
|
|
335
|
+
var widgetOrigin = this.resolveWidgetOrigin();
|
|
336
|
+
var headers = Object.assign({
|
|
337
|
+
"X-Tenant-ID": this.config.tenantId,
|
|
338
|
+
"X-Widget-Key": String(this.config.widgetKey || "").trim(),
|
|
339
|
+
"X-Target-Language": this.config.targetLanguage || "fr"
|
|
340
|
+
}, extra || {});
|
|
341
|
+
if (widgetOrigin) headers["X-Widget-Origin"] = widgetOrigin;
|
|
342
|
+
if (this.state.token) headers.Authorization = "Bearer " + this.state.token;
|
|
343
|
+
return headers;
|
|
344
|
+
},
|
|
345
|
+
parseErrorMessage: async function(response) {
|
|
346
|
+
var bodyText = "";
|
|
347
|
+
try { bodyText = await response.text(); } catch (_) {}
|
|
348
|
+
if (!bodyText) return "HTTP " + response.status;
|
|
349
|
+
try { var parsed = JSON.parse(bodyText); return parsed.error || parsed.message || ("HTTP " + response.status); } catch (_) { return bodyText.slice(0, 300); }
|
|
350
|
+
},
|
|
351
|
+
resolvePublicConfigEndpoint: function() {
|
|
352
|
+
var endpoint = String(this.config.configEndpoint || "").trim();
|
|
353
|
+
return endpoint || this.config.apiBaseUrl + "/v1/widget/config";
|
|
354
|
+
},
|
|
355
|
+
fetchPublicConfig: async function() {
|
|
356
|
+
var endpoint = this.resolvePublicConfigEndpoint();
|
|
357
|
+
var requestUrl = endpoint + (endpoint.indexOf("?") === -1 ? "?" : "&") + [
|
|
358
|
+
"tenant_id=" + encodeURIComponent(this.config.tenantId || ""),
|
|
359
|
+
"public_widget_key=" + encodeURIComponent(this.config.widgetKey || ""),
|
|
360
|
+
"origin=" + encodeURIComponent(window.location.origin || "")
|
|
361
|
+
].join("&");
|
|
362
|
+
var response = await fetch(requestUrl, { method: "GET" });
|
|
363
|
+
if (!response.ok) {
|
|
364
|
+
var errorMessage = await this.parseErrorMessage(response);
|
|
365
|
+
var error = new Error(errorMessage);
|
|
366
|
+
error.httpStatus = response.status;
|
|
367
|
+
if (response.status === 403 && this.isWidgetDisabledError(error)) {
|
|
368
|
+
this.events.emit("widget_disabled", { source: "public_config", code: "WIDGET_DISABLED" });
|
|
369
|
+
throw error;
|
|
370
|
+
}
|
|
371
|
+
throw error;
|
|
372
|
+
}
|
|
373
|
+
var payload = await response.json();
|
|
374
|
+
this.applyRuntimeConfig(payload || {}, "public_config");
|
|
375
|
+
this.events.emit("public_config_succeeded", payload || {});
|
|
376
|
+
return payload;
|
|
377
|
+
},
|
|
378
|
+
hydrateTheme: async function() {
|
|
379
|
+
if (this.state.themeHydrationPromise) return this.state.themeHydrationPromise;
|
|
380
|
+
var self = this;
|
|
381
|
+
this.state.themeHydrationPromise = this.fetchPublicConfig().catch(async function(publicConfigError) {
|
|
382
|
+
self.events.emit("public_config_failed", { error: publicConfigError && publicConfigError.message ? publicConfigError.message : "public config failed" });
|
|
383
|
+
try {
|
|
384
|
+
return await self.bootstrap();
|
|
385
|
+
} catch (bootstrapError) {
|
|
386
|
+
self.events.emit("theme_hydration_failed", {
|
|
387
|
+
public_config_error: publicConfigError && publicConfigError.message ? publicConfigError.message : "public config failed",
|
|
388
|
+
bootstrap_error: bootstrapError && bootstrapError.message ? bootstrapError.message : "bootstrap failed"
|
|
389
|
+
});
|
|
390
|
+
throw bootstrapError;
|
|
391
|
+
}
|
|
392
|
+
}).finally(function() { self.state.themeHydrationPromise = null; });
|
|
393
|
+
return this.state.themeHydrationPromise;
|
|
394
|
+
},
|
|
395
|
+
bootstrap: async function() {
|
|
396
|
+
if (this.state.token && this.isBootstrapTokenExpired()) {
|
|
397
|
+
this.state.token = "";
|
|
398
|
+
}
|
|
399
|
+
if (this.state.token) {
|
|
400
|
+
if (!this.prefetchedBootstrapAcked) {
|
|
401
|
+
this.prefetchedBootstrapAcked = true;
|
|
402
|
+
this.events.emit("bootstrap_succeeded", {
|
|
403
|
+
session_bootstrap_token: this.state.token,
|
|
404
|
+
token_expires_at: this.config.sessionBootstrapTokenExpiresAt || "",
|
|
405
|
+
preauthorized: true
|
|
406
|
+
});
|
|
407
|
+
}
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
if (this.state.bootstrapPromise) return this.state.bootstrapPromise;
|
|
411
|
+
var self = this;
|
|
412
|
+
this.state.bootstrapPromise = (async function() {
|
|
413
|
+
var turnstileToken = "";
|
|
414
|
+
if (self.turnstile && self.turnstile.isEnabled()) {
|
|
415
|
+
turnstileToken = await self.turnstile.execute();
|
|
416
|
+
self.events.emit("turnstile_validated", { token_length: turnstileToken.length });
|
|
417
|
+
}
|
|
418
|
+
var bootstrapBody = { tenant_id: self.config.tenantId, public_widget_key: self.config.widgetKey, origin: self.resolveWidgetOrigin() };
|
|
419
|
+
if (turnstileToken) bootstrapBody.cf_turnstile_token = turnstileToken;
|
|
420
|
+
return fetch(self.config.bootstrapEndpoint, {
|
|
421
|
+
method: "POST",
|
|
422
|
+
headers: { "Content-Type": "application/json" },
|
|
423
|
+
body: JSON.stringify(bootstrapBody)
|
|
424
|
+
});
|
|
425
|
+
})().then(async function(response) {
|
|
426
|
+
if (!response.ok) {
|
|
427
|
+
var errorMessage = await self.parseErrorMessage(response);
|
|
428
|
+
var error = new Error(errorMessage);
|
|
429
|
+
error.httpStatus = response.status;
|
|
430
|
+
if (response.status === 403 && self.isWidgetDisabledError(error)) {
|
|
431
|
+
self.events.emit("widget_disabled", { source: "bootstrap", code: "WIDGET_DISABLED" });
|
|
432
|
+
self.state.bootstrapPromise = null;
|
|
433
|
+
throw error;
|
|
434
|
+
}
|
|
435
|
+
throw error;
|
|
436
|
+
}
|
|
437
|
+
var payload = await response.json();
|
|
438
|
+
self.state.token = payload.session_bootstrap_token || payload.session_token || "";
|
|
439
|
+
self.config.sessionBootstrapTokenExpiresAt = String(payload.session_bootstrap_token_expires_at || payload.expires_at || "").trim();
|
|
440
|
+
self.applyRuntimeConfig(payload, "bootstrap");
|
|
441
|
+
self.events.emit("bootstrap_succeeded", payload || {});
|
|
442
|
+
return payload;
|
|
443
|
+
}).catch(function(error) {
|
|
444
|
+
if (self.isWidgetDisabledError(error)) {
|
|
445
|
+
self.events.emit("widget_disabled", { source: "bootstrap", code: "WIDGET_DISABLED" });
|
|
446
|
+
self.state.bootstrapPromise = null;
|
|
447
|
+
throw error;
|
|
448
|
+
}
|
|
449
|
+
self.events.emit("bootstrap_failed", { error: error.message || "bootstrap failed" });
|
|
450
|
+
self.state.bootstrapPromise = null;
|
|
451
|
+
throw error;
|
|
452
|
+
});
|
|
453
|
+
return this.state.bootstrapPromise;
|
|
454
|
+
},
|
|
455
|
+
ensureSession: async function() {
|
|
456
|
+
if (this.state.sessionId) return this.state.sessionId;
|
|
457
|
+
if (this.state.sessionPromise) return this.state.sessionPromise;
|
|
458
|
+
var self = this;
|
|
459
|
+
this.state.sessionPromise = (async function() {
|
|
460
|
+
await self.bootstrap();
|
|
461
|
+
var response = await self.fetchWithBootstrapRetry("create_session", self.config.chatSessionEndpoint, function() {
|
|
462
|
+
return { method: "POST", headers: self.buildHeaders({ "Content-Type": "application/json" }), body: "{}" };
|
|
463
|
+
});
|
|
464
|
+
var payload = await response.json();
|
|
465
|
+
var sessionId = payload.session_id || payload.id || "";
|
|
466
|
+
if (!sessionId) throw new Error("chat session id absent");
|
|
467
|
+
self.state.sessionId = sessionId;
|
|
468
|
+
self.state.sessionPromise = null;
|
|
469
|
+
self.events.emit("ready", payload || {});
|
|
470
|
+
return sessionId;
|
|
471
|
+
})().catch(function(error) { self.state.sessionPromise = null; throw error; });
|
|
472
|
+
return this.state.sessionPromise;
|
|
473
|
+
},
|
|
474
|
+
isBootstrapTokenExpired: function() {
|
|
475
|
+
var rawExpiry = String(this.config.sessionBootstrapTokenExpiresAt || "").trim();
|
|
476
|
+
if (!rawExpiry) return false;
|
|
477
|
+
var expiryMs = Date.parse(rawExpiry);
|
|
478
|
+
if (!Number.isFinite(expiryMs)) return false;
|
|
479
|
+
return Date.now() >= expiryMs;
|
|
480
|
+
},
|
|
481
|
+
isWidgetDisabledError: function(error) {
|
|
482
|
+
var message = String(error && error.message ? error.message : "").toLowerCase();
|
|
483
|
+
return message.indexOf("widget_disabled") !== -1;
|
|
484
|
+
},
|
|
485
|
+
shouldRetryAfterUnauthorized: function(error) {
|
|
486
|
+
if (this.isWidgetDisabledError(error)) return false;
|
|
487
|
+
var status = Number(error && error.httpStatus);
|
|
488
|
+
if (status === 401) return true;
|
|
489
|
+
var message = String(error && error.message ? error.message : "").toLowerCase();
|
|
490
|
+
return message.indexOf("401") !== -1 || message.indexOf("unauthorized") !== -1 ||
|
|
491
|
+
message.indexOf("token expired") !== -1 || message.indexOf("invalid widget token") !== -1 ||
|
|
492
|
+
message.indexOf("token bootstrap invalide") !== -1 || message.indexOf("invalide ou expir") !== -1 ||
|
|
493
|
+
message.indexOf("auth_invalid") !== -1;
|
|
494
|
+
},
|
|
495
|
+
refreshBootstrapToken: async function() {
|
|
496
|
+
this.state.token = "";
|
|
497
|
+
this.state.bootstrapPromise = null;
|
|
498
|
+
this.prefetchedBootstrapAcked = false;
|
|
499
|
+
await this.bootstrap();
|
|
500
|
+
},
|
|
501
|
+
withBootstrapRetry: async function(operationName, operation) {
|
|
502
|
+
try {
|
|
503
|
+
return await operation(0);
|
|
504
|
+
} catch (error) {
|
|
505
|
+
if (!this.shouldRetryAfterUnauthorized(error)) throw error;
|
|
506
|
+
this.events.emit("bootstrap_refresh", { operation: operationName, reason: error && error.message ? error.message : "unauthorized", refresh_attempt: 1 });
|
|
507
|
+
await this.refreshBootstrapToken();
|
|
508
|
+
return operation(1);
|
|
509
|
+
}
|
|
510
|
+
},
|
|
511
|
+
fetchWithBootstrapRetry: async function(operationName, endpointPath, buildOptions, sessionId) {
|
|
512
|
+
var self = this;
|
|
513
|
+
return this.withBootstrapRetry(operationName, async function(refreshAttempt) {
|
|
514
|
+
var options = typeof buildOptions === "function" ? buildOptions() : (buildOptions || {});
|
|
515
|
+
var response = await fetch(self.resolveRuntimeUrl(endpointPath, sessionId), options);
|
|
516
|
+
if (!response.ok) {
|
|
517
|
+
var message = await self.parseErrorMessage(response);
|
|
518
|
+
var error = new Error(refreshAttempt > 0 ? message + " (refresh_attempt=" + refreshAttempt + ")" : message);
|
|
519
|
+
error.httpStatus = response.status;
|
|
520
|
+
throw error;
|
|
521
|
+
}
|
|
522
|
+
return response;
|
|
523
|
+
});
|
|
524
|
+
},
|
|
525
|
+
postMessage: async function(content) {
|
|
526
|
+
var sessionId = await this.ensureSession();
|
|
527
|
+
var self = this;
|
|
528
|
+
await this.fetchWithBootstrapRetry("post_message", this.config.chatMessageTemplate, function() {
|
|
529
|
+
return { method: "POST", headers: self.buildHeaders({ "Content-Type": "application/json" }), body: JSON.stringify({ content: content }) };
|
|
530
|
+
}, sessionId);
|
|
531
|
+
return sessionId;
|
|
532
|
+
},
|
|
533
|
+
consumeSSE: function(reader, callbacks, allowedEvents) {
|
|
534
|
+
var decoder = new TextDecoder();
|
|
535
|
+
var pending = "";
|
|
536
|
+
var currentEvent = "";
|
|
537
|
+
var allowed = {};
|
|
538
|
+
allowedEvents.forEach(function(name) { allowed[name] = true; });
|
|
539
|
+
function dispatch(eventName, payload) {
|
|
540
|
+
if (!allowed[eventName]) return;
|
|
541
|
+
if (eventName === "token" && callbacks.onToken) callbacks.onToken(payload.value || "");
|
|
542
|
+
if (eventName === "final" && callbacks.onFinal) callbacks.onFinal(payload);
|
|
543
|
+
if (eventName === "done" && callbacks.onDone) callbacks.onDone(payload);
|
|
544
|
+
if (eventName === "error" && callbacks.onError) callbacks.onError(payload.message || "Erreur de streaming");
|
|
545
|
+
if (eventName === "transcription_draft_delta" && callbacks.onDelta) callbacks.onDelta(payload);
|
|
546
|
+
if (eventName === "transcription_draft_complete" && callbacks.onComplete) callbacks.onComplete(payload);
|
|
547
|
+
if (eventName === "transcription_draft_error" && callbacks.onError) callbacks.onError(payload);
|
|
548
|
+
}
|
|
549
|
+
return new Promise(function(resolve, reject) {
|
|
550
|
+
function step() {
|
|
551
|
+
reader.read().then(function(result) {
|
|
552
|
+
if (result.done) return resolve();
|
|
553
|
+
pending += decoder.decode(result.value, { stream: true });
|
|
554
|
+
var lines = pending.split("\n");
|
|
555
|
+
pending = lines.pop() || "";
|
|
556
|
+
lines.forEach(function(line) {
|
|
557
|
+
if (line.startsWith("event: ")) currentEvent = line.slice(7).trim();
|
|
558
|
+
if (line.startsWith("data: ")) {
|
|
559
|
+
try { dispatch(currentEvent, JSON.parse(line.slice(6))); } catch (_) {}
|
|
560
|
+
currentEvent = "";
|
|
561
|
+
}
|
|
562
|
+
});
|
|
563
|
+
step();
|
|
564
|
+
}).catch(reject);
|
|
565
|
+
}
|
|
566
|
+
step();
|
|
567
|
+
});
|
|
568
|
+
},
|
|
569
|
+
openEventStream: async function(operationName, endpointPath, sessionId, callbacks, allowedEvents, requestOptions) {
|
|
570
|
+
var self = this;
|
|
571
|
+
var response = await this.fetchWithBootstrapRetry(operationName, endpointPath, function() {
|
|
572
|
+
var options = { method: "GET", headers: self.buildHeaders() };
|
|
573
|
+
if (requestOptions) Object.assign(options, requestOptions);
|
|
574
|
+
return options;
|
|
575
|
+
}, sessionId);
|
|
576
|
+
if (!response.body) throw new Error("stream response body is missing");
|
|
577
|
+
return this.consumeSSE(response.body.getReader(), callbacks, allowedEvents);
|
|
578
|
+
},
|
|
579
|
+
streamSession: async function(sessionId, callbacks) {
|
|
580
|
+
return this.openEventStream("stream_session", this.config.chatStreamTemplate, sessionId, callbacks, ["token", "final", "error", "done"]);
|
|
581
|
+
},
|
|
582
|
+
streamAudioDraft: async function(sessionId, callbacks) {
|
|
583
|
+
var self = this;
|
|
584
|
+
var controller = new AbortController();
|
|
585
|
+
var response = await this.fetchWithBootstrapRetry("audio_stream", this.config.chatAudioStreamTemplate, function() {
|
|
586
|
+
return { method: "GET", headers: self.buildHeaders(), signal: controller.signal };
|
|
587
|
+
}, sessionId);
|
|
588
|
+
if (!response.body) throw new Error("stream response body is missing");
|
|
589
|
+
var consumePromise = this.consumeSSE(response.body.getReader(), callbacks,
|
|
590
|
+
["transcription_draft_delta", "transcription_draft_complete", "transcription_draft_error"]);
|
|
591
|
+
consumePromise.catch(function(error) {
|
|
592
|
+
if (self.isExpectedStreamAbortError(error)) return;
|
|
593
|
+
if (callbacks.onTransportError) callbacks.onTransportError(error.message || "Erreur stream audio");
|
|
594
|
+
});
|
|
595
|
+
return function() { controller.abort(); };
|
|
596
|
+
},
|
|
597
|
+
isExpectedStreamAbortError: function(error) {
|
|
598
|
+
var message = String(error && error.message ? error.message : "").toLowerCase();
|
|
599
|
+
return message.indexOf("aborted") !== -1 ||
|
|
600
|
+
message.indexOf("aborterror") !== -1 ||
|
|
601
|
+
message.indexOf("body stream buffer was aborted") !== -1 ||
|
|
602
|
+
message.indexOf("signal is aborted") !== -1;
|
|
603
|
+
},
|
|
604
|
+
postAudioJson: async function(operationName, endpointPath, sessionId, body) {
|
|
605
|
+
var self = this;
|
|
606
|
+
return this.fetchWithBootstrapRetry(operationName, endpointPath, function() {
|
|
607
|
+
return { method: "POST", headers: self.buildHeaders({ "Content-Type": "application/json" }), body: JSON.stringify(body) };
|
|
608
|
+
}, sessionId);
|
|
609
|
+
},
|
|
610
|
+
startAudioSession: async function(sessionId) {
|
|
611
|
+
return (await this.postAudioJson("audio_session_start", this.config.chatAudioSessionTemplate, sessionId, { language: "fr", realtime: false, model: "voxtral-mini-latest" })).json();
|
|
612
|
+
},
|
|
613
|
+
sendAudioChunk: async function(sessionId, audioSessionId, chunk) {
|
|
614
|
+
await this.postAudioJson("audio_chunk_send", this.config.chatAudioChunkTemplate, sessionId, { audio_session_id: audioSessionId, chunk: chunk, index: 0 });
|
|
615
|
+
},
|
|
616
|
+
closeAudioSession: async function(sessionId, audioSessionId) {
|
|
617
|
+
await this.postAudioJson("audio_session_close", this.config.chatAudioCloseTemplate, sessionId, { audio_session_id: audioSessionId });
|
|
618
|
+
}
|
|
619
|
+
});
|
|
620
|
+
function buildConfig(scriptTag) {
|
|
621
|
+
var themeSnapshot = parseThemeSnapshot(readScriptAttr(scriptTag, "data-theme-snapshot", ""));
|
|
622
|
+
var initialThemeVersion = normalizeThemeVersion(readScriptAttr(scriptTag, "data-theme-version", ""));
|
|
623
|
+
var config = {
|
|
624
|
+
themeSplitEnabled: false,
|
|
625
|
+
hostPrefersDark: resolveHostPrefersDark(false),
|
|
626
|
+
effectiveThemeScheme: "light",
|
|
627
|
+
hasSnapshotTheme: false,
|
|
628
|
+
themeVersion: initialThemeVersion,
|
|
629
|
+
disclaimer: readScriptAttr(scriptTag, "data-disclaimer", "true") !== "false"
|
|
630
|
+
};
|
|
631
|
+
CONFIG_STRING_ATTRS.forEach(function(row) {
|
|
632
|
+
var value = readScriptAttr(scriptTag, row[1], row[2]);
|
|
633
|
+
config[row[0]] = row[0] === "apiBaseUrl" || row[0] === "widgetOrigin" ? trimTrailingSlash(value) : value;
|
|
634
|
+
});
|
|
635
|
+
config.primaryColor = readScriptHexAttr(scriptTag, "data-primary-color", "#0066ff");
|
|
636
|
+
config.position = normalizePosition(readScriptAttr(scriptTag, "data-position", ""), "bottom-right");
|
|
637
|
+
config.panelBackgroundColor = readScriptHexAttr(scriptTag, "data-panel-background-color", "#ffffff");
|
|
638
|
+
config.panelBackgroundAlpha = clampNumericString(readScriptAttr(scriptTag, "data-panel-background-alpha", ""), "1", 0, 1, false);
|
|
639
|
+
config.panelBorderRadius = clampNumericString(readScriptAttr(scriptTag, "data-panel-border-radius", ""), "16", 0, 32, true);
|
|
640
|
+
config.assistantName = String(readScriptAttr(scriptTag, "data-assistant-name", "Assistant mairie")).trim() || "Assistant mairie";
|
|
641
|
+
config.targetLanguage = String(readScriptAttr(scriptTag, "data-target-language", "fr")).trim() || "fr";
|
|
642
|
+
config.themeModeStrategy = normalizeThemeModeStrategy(readScriptAttr(scriptTag, "data-theme-mode-strategy", ""));
|
|
643
|
+
applyScriptMascotColors(config, scriptTag);
|
|
644
|
+
if (themeSnapshot && applyThemeConfigToRuntime(config, themeSnapshot)) {
|
|
645
|
+
config.hasSnapshotTheme = true;
|
|
646
|
+
if (normalizeThemeVersion(config.themeVersion) === 0) {
|
|
647
|
+
config.themeVersion = normalizeThemeVersion(themeSnapshot.theme_updated_at);
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
config.effectiveThemeScheme = resolvePreferredThemeScheme(config);
|
|
651
|
+
if (!config.apiBaseUrl) config.apiBaseUrl = config.widgetOrigin || trimTrailingSlash(window.location.origin);
|
|
652
|
+
if (!config.bootstrapEndpoint) config.bootstrapEndpoint = config.apiBaseUrl + "/v1/occe/chat/bootstrap";
|
|
653
|
+
if (!config.configEndpoint) config.configEndpoint = config.apiBaseUrl + "/v1/widget/config";
|
|
654
|
+
if (!config.mascotUrl) config.mascotUrl = "/v1/widget/assets/chouette_ourlu.svg";
|
|
655
|
+
config.mascotUrl = resolveAbsoluteMascotUrl(config.mascotUrl, config.apiBaseUrl);
|
|
656
|
+
return config;
|
|
657
|
+
}
|
|
658
|
+
function mountFromScript(scriptTag) {
|
|
659
|
+
if (!scriptTag || !scriptTag.getAttribute("data-tenant-id")) {
|
|
660
|
+
console.error("[OurluMairie] Loader: data-tenant-id is required.");
|
|
661
|
+
return;
|
|
662
|
+
}
|
|
663
|
+
if (document.getElementById(CONTAINER_ID)) return;
|
|
664
|
+
var config = buildConfig(scriptTag);
|
|
665
|
+
var events = new EventBus();
|
|
666
|
+
var state = { open: false, sessionId: "", token: config.sessionBootstrapToken || "", sending: false, listening: false, bootstrapPromise: null, sessionPromise: null };
|
|
667
|
+
var ui = new WidgetUIManager(config);
|
|
668
|
+
if (!ui.mount()) return;
|
|
669
|
+
var turnstile = config.turnstileSiteKey ? new TurnstileChallengeManager(config.turnstileSiteKey) : null;
|
|
670
|
+
var api = new WidgetApiManager(config, state, events, ui, turnstile);
|
|
671
|
+
var audio = new WidgetAudioManager(state, api, ui);
|
|
672
|
+
if (!audio.isSupported()) ui.micButton.style.display = "none";
|
|
673
|
+
api.hydrateTheme().catch(function() {});
|
|
674
|
+
var mediaQueryList = null;
|
|
675
|
+
var handleHostThemeChange = null;
|
|
676
|
+
if (typeof window.matchMedia === "function") {
|
|
677
|
+
mediaQueryList = window.matchMedia("(prefers-color-scheme: dark)");
|
|
678
|
+
handleHostThemeChange = function(event) {
|
|
679
|
+
if (normalizeThemeModeStrategy(config.themeModeStrategy) !== "auto_follow_host") return;
|
|
680
|
+
config.hostPrefersDark = Boolean(event && event.matches);
|
|
681
|
+
config.effectiveThemeScheme = resolvePreferredThemeScheme(config);
|
|
682
|
+
ui.updateTheme(config);
|
|
683
|
+
};
|
|
684
|
+
if (typeof mediaQueryList.addEventListener === "function") mediaQueryList.addEventListener("change", handleHostThemeChange);
|
|
685
|
+
else if (typeof mediaQueryList.addListener === "function") mediaQueryList.addListener(handleHostThemeChange);
|
|
686
|
+
}
|
|
687
|
+
async function handleSend(event) {
|
|
688
|
+
event.preventDefault();
|
|
689
|
+
if (state.sending || state.listening) return;
|
|
690
|
+
var message = ui.pullInput();
|
|
691
|
+
if (!message) return;
|
|
692
|
+
state.sending = true;
|
|
693
|
+
ui.showError("");
|
|
694
|
+
ui.addMessage("user", message);
|
|
695
|
+
ui.showTyping(true);
|
|
696
|
+
ui.setComposerDisabled(true);
|
|
697
|
+
try {
|
|
698
|
+
var sessionId = await api.postMessage(message);
|
|
699
|
+
ui.startAssistantStream();
|
|
700
|
+
await api.streamSession(sessionId, {
|
|
701
|
+
onToken: function(token) { ui.appendAssistantToken(token); },
|
|
702
|
+
onFinal: function(payload) { ui.finalizeAssistantMessage(payload.content || ""); },
|
|
703
|
+
onError: function(errorText) { ui.showError(errorText || "Erreur de streaming."); },
|
|
704
|
+
onDone: function() {}
|
|
705
|
+
});
|
|
706
|
+
} catch (error) {
|
|
707
|
+
var sendErrorMessage = (error && error.message) || "Impossible d'envoyer le message.";
|
|
708
|
+
ui.showRetryableError(sendErrorMessage, function() {
|
|
709
|
+
handleSend(event);
|
|
710
|
+
});
|
|
711
|
+
} finally {
|
|
712
|
+
state.sending = false;
|
|
713
|
+
ui.showTyping(false);
|
|
714
|
+
ui.setComposerDisabled(false);
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
function togglePanel() {
|
|
718
|
+
state.open = !state.open;
|
|
719
|
+
ui.setOpen(state.open);
|
|
720
|
+
events.emit(state.open ? "open" : "close", {});
|
|
721
|
+
if (state.open) {
|
|
722
|
+
api.ensureSession().catch(function(error) {
|
|
723
|
+
var errorMessage = (error && error.message) || "Impossible d'initialiser le chat.";
|
|
724
|
+
ui.showRetryableError(errorMessage, function() {
|
|
725
|
+
state.sessionId = "";
|
|
726
|
+
state.token = "";
|
|
727
|
+
state.bootstrapPromise = null;
|
|
728
|
+
state.sessionPromise = null;
|
|
729
|
+
if (turnstile) turnstile.invalidateCache();
|
|
730
|
+
api.prefetchedBootstrapAcked = false;
|
|
731
|
+
api.ensureSession().catch(function(retryError) {
|
|
732
|
+
ui.showRetryableError((retryError && retryError.message) || "Impossible d'initialiser le chat.", arguments.callee);
|
|
733
|
+
});
|
|
734
|
+
});
|
|
735
|
+
});
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
ui.bind({
|
|
739
|
+
onToggle: togglePanel,
|
|
740
|
+
onSend: handleSend,
|
|
741
|
+
onMicToggle: function() {
|
|
742
|
+
audio.toggle().catch(function(error) {
|
|
743
|
+
state.listening = false;
|
|
744
|
+
ui.setMicListening(false);
|
|
745
|
+
ui.showError((error && error.message) || "Erreur micro.");
|
|
746
|
+
});
|
|
747
|
+
},
|
|
748
|
+
onEscape: function(event) { if (event.key === "Escape" && state.open) togglePanel(); }
|
|
749
|
+
});
|
|
750
|
+
var instance = { version: WIDGET_VERSION, open: function() { if (!state.open) togglePanel(); }, close: function() { if (state.open) togglePanel(); }, destroy: function() {
|
|
751
|
+
if (mediaQueryList && handleHostThemeChange) {
|
|
752
|
+
if (typeof mediaQueryList.removeEventListener === "function") mediaQueryList.removeEventListener("change", handleHostThemeChange);
|
|
753
|
+
else if (typeof mediaQueryList.removeListener === "function") mediaQueryList.removeListener(handleHostThemeChange);
|
|
754
|
+
}
|
|
755
|
+
audio.cleanupMedia();
|
|
756
|
+
if (ui.host && ui.host.parentNode) ui.host.parentNode.removeChild(ui.host);
|
|
757
|
+
}, on: function(eventName, handler) { events.on(eventName, handler); } };
|
|
758
|
+
events.on("widget_disabled", function() {
|
|
759
|
+
instance.destroy();
|
|
760
|
+
window[WIDGET_NS] = null;
|
|
761
|
+
window.OurluWidget = null;
|
|
762
|
+
});
|
|
763
|
+
window[WIDGET_NS] = instance;
|
|
764
|
+
window.OurluWidget = { instance: instance };
|
|
765
|
+
}
|
|
766
|
+
runtime.mountFromScript = mountFromScript;
|
|
767
|
+
})();
|