@ourlu/assistant-sdk 0.2.5 → 0.2.6

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.
@@ -51,6 +51,12 @@ function normalizeHexColor(rawValue, fallbackColor) {
51
51
  var normalized = String(rawValue || "").trim().toLowerCase();
52
52
  return /^#[0-9a-f]{6}$/.test(normalized) ? normalized : fallbackColor;
53
53
  }
54
+ function normalizeLabelBool(rawValue, fallbackValue) {
55
+ var normalized = String(rawValue || "").trim().toLowerCase();
56
+ if (normalized === "true") return true;
57
+ if (normalized === "false") return false;
58
+ return Boolean(fallbackValue);
59
+ }
54
60
  function normalizePosition(rawValue, fallbackValue) {
55
61
  var normalized = String(rawValue || "").trim().toLowerCase();
56
62
  return normalized === "bottom-left" || normalized === "bottom-right" ? normalized : fallbackValue;
@@ -129,6 +135,18 @@ function applyThemeConfigToRuntime(config, rawWidgetConfig) {
129
135
  config.panelBackgroundColor = normalizeHexColor(labels.panel_background_color, config.panelBackgroundColor || "#ffffff");
130
136
  config.panelBackgroundAlpha = clampNumericString(labels.panel_background_alpha, config.panelBackgroundAlpha || "1", 0, 1, false);
131
137
  config.panelBorderRadius = clampNumericString(labels.panel_border_radius, config.panelBorderRadius || "16", 0, 32, true);
138
+ config.headerGradientEnabled = normalizeLabelBool(labels.header_gradient_enabled, config.headerGradientEnabled !== false);
139
+ config.headerGradientColor2Auto = normalizeLabelBool(labels.header_gradient_color2_auto, config.headerGradientColor2Auto !== false);
140
+ config.headerGradientColor2 = normalizeHexColor(labels.header_gradient_color2, config.headerGradientColor2 || "#0047b3");
141
+ config.userBubbleColor = normalizeHexColor(labels.user_bubble_color, config.userBubbleColor || "");
142
+ config.userBubbleGradientEnabled = normalizeLabelBool(labels.user_bubble_gradient_enabled, config.userBubbleGradientEnabled === true);
143
+ config.userBubbleGradientColor2Auto = normalizeLabelBool(labels.user_bubble_gradient_color2_auto, config.userBubbleGradientColor2Auto !== false);
144
+ config.userBubbleGradientColor2 = normalizeHexColor(labels.user_bubble_gradient_color2, config.userBubbleGradientColor2 || "");
145
+ config.assistantBubbleColor = normalizeHexColor(labels.assistant_bubble_color, config.assistantBubbleColor || "#f0f4f8");
146
+ config.assistantBubbleTextColor = normalizeHexColor(labels.assistant_bubble_text_color, config.assistantBubbleTextColor || "#1a1a2e");
147
+ config.assistantBubbleGradientEnabled = normalizeLabelBool(labels.assistant_bubble_gradient_enabled, config.assistantBubbleGradientEnabled === true);
148
+ config.assistantBubbleGradientColor2Auto = normalizeLabelBool(labels.assistant_bubble_gradient_color2_auto, config.assistantBubbleGradientColor2Auto !== false);
149
+ config.assistantBubbleGradientColor2 = normalizeHexColor(labels.assistant_bubble_gradient_color2, config.assistantBubbleGradientColor2 || "");
132
150
  applyMascotLabelColors(config, labels);
133
151
  var candidateVersion = normalizeThemeVersion(rawWidgetConfig.theme_updated_at);
134
152
  if (candidateVersion > 0) config.themeVersion = candidateVersion;
@@ -540,6 +558,7 @@ Object.assign(WidgetApiManager.prototype, {
540
558
  if (eventName === "done" && callbacks.onDone) callbacks.onDone(payload);
541
559
  if (eventName === "error" && callbacks.onError) callbacks.onError(payload.message || "Erreur de streaming");
542
560
  if (eventName === "module_signalement" && callbacks.onModuleSignalement) callbacks.onModuleSignalement(payload);
561
+ if (eventName === "module_plu" && callbacks.onModulePlu) callbacks.onModulePlu(payload);
543
562
  if (eventName === "transcription_draft_delta" && callbacks.onDelta) callbacks.onDelta(payload);
544
563
  if (eventName === "transcription_draft_complete" && callbacks.onComplete) callbacks.onComplete(payload);
545
564
  if (eventName === "transcription_draft_error" && callbacks.onError) callbacks.onError(payload);
@@ -575,7 +594,7 @@ Object.assign(WidgetApiManager.prototype, {
575
594
  return this.consumeSSE(response.body.getReader(), callbacks, allowedEvents);
576
595
  },
577
596
  streamSession: async function(sessionId, callbacks) {
578
- return this.openEventStream("stream_session", this.config.chatStreamTemplate, sessionId, callbacks, ["token", "final", "error", "done", "module_signalement"]);
597
+ return this.openEventStream("stream_session", this.config.chatStreamTemplate, sessionId, callbacks, ["token", "final", "error", "done", "module_signalement", "module_plu"]);
579
598
  },
580
599
  streamAudioDraft: async function(sessionId, callbacks) {
581
600
  var self = this;
@@ -676,6 +695,12 @@ function mountFromScript(scriptTag) {
676
695
  signalementModule = new SignalementModuleManager(ui.getRoot(), events, api, ui);
677
696
  signalementModule.mount(ui.getPanel(), ui.getHeader());
678
697
  }
698
+ var pluModule = null;
699
+ var PluModuleManager = runtime.PluModuleManager;
700
+ if (PluModuleManager) {
701
+ pluModule = new PluModuleManager(ui.getRoot(), events, api, ui);
702
+ pluModule.mount();
703
+ }
679
704
  var mediaQueryList = null;
680
705
  var handleHostThemeChange = null;
681
706
  if (typeof window.matchMedia === "function") {
@@ -690,6 +715,7 @@ function mountFromScript(scriptTag) {
690
715
  else if (typeof mediaQueryList.addListener === "function") mediaQueryList.addListener(handleHostThemeChange);
691
716
  }
692
717
  var signalementActive = false;
718
+ var pluActive = false;
693
719
 
694
720
  if (signalementModule) {
695
721
  events.on("module:signalement:open", function() { signalementActive = true; });
@@ -702,6 +728,11 @@ function mountFromScript(scriptTag) {
702
728
  });
703
729
  }
704
730
 
731
+ if (pluModule) {
732
+ events.on("module:plu:open", function() { pluActive = true; });
733
+ events.on("module:plu:closed", function() { pluActive = false; });
734
+ }
735
+
705
736
  async function handleSignalementSubmit() {
706
737
  if (!signalementModule || !signalementActive) return;
707
738
  state.sending = true;
@@ -738,20 +769,30 @@ function mountFromScript(scriptTag) {
738
769
  var sessionId = await api.postMessage(message);
739
770
  ui.startAssistantStream();
740
771
  var pendingSignalementData = null;
772
+ var pendingPluData = null;
741
773
  await api.streamSession(sessionId, {
742
774
  onToken: function(token) { ui.appendAssistantToken(token); },
743
775
  onModuleSignalement: function(payload) {
744
776
  pendingSignalementData = payload || {};
745
777
  },
778
+ onModulePlu: function(payload) {
779
+ pendingPluData = payload || {};
780
+ },
746
781
  onFinal: function(payload) {
747
782
  ui.finalizeAssistantMessage(payload.content || "");
783
+ var streamEl = ui.messages.lastElementChild;
748
784
  var sigData = pendingSignalementData || (payload.module_signalement ? payload.module_signalement : null);
749
785
  if (signalementModule && sigData) {
750
- var streamEl = ui.messages.lastElementChild;
751
786
  ui.injectSignalementButton(streamEl, function() {
752
787
  signalementModule.open(sigData);
753
788
  });
754
789
  }
790
+ var pluData = pendingPluData || (payload.module_plu ? payload.module_plu : null);
791
+ if (pluModule && pluData) {
792
+ ui.injectPluButton(streamEl, function() {
793
+ pluModule.open();
794
+ });
795
+ }
755
796
  },
756
797
  onError: function(errorText) { ui.showError(errorText || "Erreur de streaming."); },
757
798
  onDone: function() {}
@@ -1,6 +1,6 @@
1
1
  (function() {
2
2
  "use strict";
3
- var __WIDGET_MANIFEST__ = {"ui.v1.js":"ui.v1.e007c7c4.js","audio.v1.js":"audio.v1.20858b08.js","signalement.v1.js":"signalement.v1.d321dfde.js","engine.v1.js":"engine.v1.c127656e.js"};
3
+ var __WIDGET_MANIFEST__ = {"ui.v1.js":"ui.v1.0caedc90.js","audio.v1.js":"audio.v1.20858b08.js","signalement.v1.js":"signalement.v1.d321dfde.js","plu.v1.js":"plu.v1.cc853a2d.js","engine.v1.js":"engine.v1.19c589a2.js"};
4
4
 
5
5
 
6
6
  var RUNTIME_NS = "__OurluWidgetRuntimeV1";
@@ -46,17 +46,21 @@
46
46
  var uiFileName = resolveManifestFileName("ui.v1.js");
47
47
  var audioFileName = resolveManifestFileName("audio.v1.js");
48
48
  var signalementFileName = resolveManifestFileName("signalement.v1.js");
49
+ var pluFileName = resolveManifestFileName("plu.v1.js");
49
50
  var engineFileName = resolveManifestFileName("engine.v1.js");
50
51
 
51
52
  var uiRuntimeUrl = scriptTag.getAttribute("data-runtime-ui-url") || buildApiRuntimeAssetUrl(scriptTag, uiFileName);
52
53
  var audioRuntimeUrl = scriptTag.getAttribute("data-runtime-audio-url") || buildApiRuntimeAssetUrl(scriptTag, audioFileName);
53
54
  var signalementRuntimeUrl = scriptTag.getAttribute("data-runtime-signalement-url") || buildApiRuntimeAssetUrl(scriptTag, signalementFileName);
55
+ var pluRuntimeUrl = scriptTag.getAttribute("data-runtime-plu-url") || buildApiRuntimeAssetUrl(scriptTag, pluFileName);
54
56
  var engineRuntimeUrl = scriptTag.getAttribute("data-runtime-engine-url") || buildApiRuntimeAssetUrl(scriptTag, engineFileName);
55
57
 
56
58
  window[RUNTIME_FLAG] = loadScript(uiRuntimeUrl).then(function() {
57
59
  return loadScript(audioRuntimeUrl);
58
60
  }).then(function() {
59
61
  return loadScript(signalementRuntimeUrl);
62
+ }).then(function() {
63
+ return loadScript(pluRuntimeUrl);
60
64
  }).then(function() {
61
65
  return loadScript(engineRuntimeUrl);
62
66
  });
@@ -0,0 +1,458 @@
1
+ (function() {
2
+ "use strict";
3
+ var runtime = window.__OurluWidgetRuntimeV1 || (window.__OurluWidgetRuntimeV1 = {});
4
+
5
+ var BAN_API_URL = "https://api-adresse.data.gouv.fr/search/";
6
+ var BAN_DEBOUNCE_MS = 300;
7
+ var BAN_MAX_RESULTS = 5;
8
+
9
+ function PluCssBuilder() {}
10
+
11
+ PluCssBuilder.prototype.build = function() {
12
+ return [
13
+ this.buildContent(), this.buildAddressInput(), this.buildSuggestions(),
14
+ this.buildSearchButton(), this.buildLoading(), this.buildResult(),
15
+ this.buildError(), this.buildPluButton(), this.buildMobileOverrides()
16
+ ].join("\n");
17
+ };
18
+
19
+ PluCssBuilder.prototype.buildContent = function() {
20
+ return [
21
+ ".cm-tab[data-tab='plu'].hidden{display:none}",
22
+ "#cm-plu-content{display:none;flex-direction:column;flex:1;overflow:hidden}",
23
+ "#cm-plu-content.active{display:flex}",
24
+ ".cm-plu-scroll{flex:1;overflow-y:auto;overflow-x:hidden;padding:20px 16px;display:flex;flex-direction:column;gap:16px;min-width:0;width:100%}",
25
+ ".cm-plu-scroll::-webkit-scrollbar{width:6px}",
26
+ ".cm-plu-scroll::-webkit-scrollbar-thumb{background:rgba(0,0,0,.1);border-radius:3px}",
27
+ ".cm-plu-title{display:flex;align-items:center;gap:8px;font-size:16px;font-weight:600;color:#1e293b;margin:0}",
28
+ ".cm-plu-title svg{width:20px;height:20px;flex-shrink:0}",
29
+ ".cm-plu-desc{font-size:14px;color:#64748b;line-height:1.5;margin:0}"
30
+ ].join("\n");
31
+ };
32
+
33
+ PluCssBuilder.prototype.buildAddressInput = function() {
34
+ return [
35
+ ".cm-plu-input-wrap{position:relative}",
36
+ ".cm-plu-input{width:100%;padding:12px 14px;border:1.5px solid #e2e8f0;border-radius:12px;font-size:14px;font-family:inherit;outline:none;transition:border-color .2s,box-shadow .2s;background:#fff;color:#1e293b;box-sizing:border-box}",
37
+ ".cm-plu-input:focus{border-color:#0066ff;box-shadow:0 0 0 3px rgba(0,102,255,.15)}",
38
+ ".cm-plu-input::placeholder{color:#9ca3af}"
39
+ ].join("\n");
40
+ };
41
+
42
+ PluCssBuilder.prototype.buildSuggestions = function() {
43
+ return [
44
+ ".cm-plu-suggestions{position:absolute;top:100%;left:0;right:0;margin-top:4px;background:#fff;border:1.5px solid #e2e8f0;border-radius:10px;box-shadow:0 8px 24px rgba(0,0,0,.12);z-index:10;overflow:hidden;display:none}",
45
+ ".cm-plu-suggestions.visible{display:block}",
46
+ ".cm-plu-suggestion{padding:10px 14px;font-size:14px;color:#334155;cursor:pointer;transition:background .1s;border-bottom:1px solid #f1f5f9;font-family:inherit}",
47
+ ".cm-plu-suggestion:last-child{border-bottom:none}",
48
+ ".cm-plu-suggestion:hover,.cm-plu-suggestion.highlighted{background:#eff6ff;color:#1d4ed8}"
49
+ ].join("\n");
50
+ };
51
+
52
+ PluCssBuilder.prototype.buildSearchButton = function() {
53
+ return [
54
+ ".cm-plu-search-btn{display:flex;align-items:center;justify-content:center;gap:8px;width:100%;padding:12px 20px;border-radius:12px;border:none;background:#0066ff;color:#fff;font-size:14px;font-weight:600;cursor:pointer;transition:all .15s;font-family:inherit}",
55
+ ".cm-plu-search-btn svg{width:18px;height:18px;flex-shrink:0}",
56
+ ".cm-plu-search-btn:hover:not(:disabled){background:#0052cc;transform:translateY(-1px);box-shadow:0 4px 12px rgba(0,102,255,.3)}",
57
+ ".cm-plu-search-btn:active:not(:disabled){transform:scale(0.97)}",
58
+ ".cm-plu-search-btn:disabled{opacity:.5;cursor:default}"
59
+ ].join("\n");
60
+ };
61
+
62
+ PluCssBuilder.prototype.buildLoading = function() {
63
+ return [
64
+ ".cm-plu-loading{display:none;flex-direction:column;align-items:center;gap:12px;padding:24px 16px;text-align:center}",
65
+ ".cm-plu-loading.visible{display:flex}",
66
+ ".cm-plu-spinner{width:36px;height:36px;border:3px solid #e2e8f0;border-top-color:#0066ff;border-radius:50%;animation:cm-plu-spin 1s linear infinite}",
67
+ "@keyframes cm-plu-spin{to{transform:rotate(360deg)}}",
68
+ ".cm-plu-loading-text{font-size:14px;color:#64748b;font-weight:500}",
69
+ ".cm-plu-loading-sub{font-size:13px;color:#94a3b8}"
70
+ ].join("\n");
71
+ };
72
+
73
+ PluCssBuilder.prototype.buildResult = function() {
74
+ return [
75
+ ".cm-plu-result{display:none;flex-direction:column;gap:16px}",
76
+ ".cm-plu-result.visible{display:flex}",
77
+ ".cm-plu-result-addr{display:flex;align-items:flex-start;gap:8px;font-size:14px;color:#334155;line-height:1.5}",
78
+ ".cm-plu-result-addr svg{width:18px;height:18px;flex-shrink:0;color:#0066ff;margin-top:2px}",
79
+ ".cm-plu-zone-card{padding:14px 16px;border-radius:12px;background:#eff6ff;border:1.5px solid #bfdbfe;display:flex;flex-direction:column;gap:4px}",
80
+ ".cm-plu-zone-code{font-size:18px;font-weight:700;color:#1d4ed8}",
81
+ ".cm-plu-zone-label{font-size:14px;color:#3b82f6;font-weight:500}",
82
+ ".cm-plu-zone-doc{font-size:12px;color:#64748b;margin-top:2px}",
83
+ ".cm-plu-rules-title{font-size:13px;font-weight:600;color:#475569;text-transform:uppercase;letter-spacing:.04em;margin-bottom:4px}",
84
+ ".cm-plu-rules-list{list-style:disc;padding-left:20px;margin:0;display:flex;flex-direction:column;gap:6px}",
85
+ ".cm-plu-rules-list li{font-size:14px;color:#334155;line-height:1.5}",
86
+ ".cm-plu-sources{font-size:12px;color:#94a3b8;margin-top:4px}",
87
+ ".cm-plu-disclaimer{display:flex;align-items:flex-start;gap:8px;padding:12px 14px;border-radius:10px;background:#fffbeb;border:1px solid #fde68a;font-size:13px;color:#92400e;line-height:1.5}",
88
+ ".cm-plu-disclaimer svg{width:16px;height:16px;flex-shrink:0;margin-top:2px}",
89
+ ".cm-plu-new-search-btn{display:flex;align-items:center;justify-content:center;gap:8px;width:100%;padding:10px 20px;border-radius:12px;border:1.5px solid #e2e8f0;background:#fff;color:#475569;font-size:14px;font-weight:500;cursor:pointer;transition:all .15s;font-family:inherit}",
90
+ ".cm-plu-new-search-btn:hover{border-color:#94a3b8;background:#f8fafc;color:#334155}"
91
+ ].join("\n");
92
+ };
93
+
94
+ PluCssBuilder.prototype.buildError = function() {
95
+ return [
96
+ ".cm-plu-error{display:none;flex-direction:column;align-items:center;gap:12px;padding:20px 16px;text-align:center}",
97
+ ".cm-plu-error.visible{display:flex}",
98
+ ".cm-plu-error-icon{width:40px;height:40px;border-radius:50%;background:#fef2f2;display:flex;align-items:center;justify-content:center}",
99
+ ".cm-plu-error-icon svg{width:22px;height:22px;color:#dc2626}",
100
+ ".cm-plu-error-msg{font-size:14px;color:#991b1b;font-weight:500;line-height:1.5}",
101
+ ".cm-plu-error-code{font-size:12px;color:#94a3b8;font-family:ui-monospace,monospace}",
102
+ ".cm-plu-retry-btn{display:flex;align-items:center;justify-content:center;gap:6px;padding:10px 20px;border-radius:12px;border:1.5px solid #fecaca;background:#fef2f2;color:#dc2626;font-size:14px;font-weight:500;cursor:pointer;transition:all .15s;font-family:inherit}",
103
+ ".cm-plu-retry-btn:hover{background:#dc2626;color:#fff;border-color:#dc2626}"
104
+ ].join("\n");
105
+ };
106
+
107
+ PluCssBuilder.prototype.buildPluButton = function() {
108
+ return [
109
+ ".cm-plu-open-btn{display:inline-flex;align-items:center;gap:8px;margin-top:10px;padding:10px 18px;border-radius:12px;border:1.5px solid #0066ff;background:#eff6ff;color:#0066ff;font-size:14px;font-weight:600;cursor:pointer;transition:all .15s;font-family:inherit}",
110
+ ".cm-plu-open-btn svg{width:18px;height:18px;flex-shrink:0}",
111
+ ".cm-plu-open-btn:hover{background:#0066ff;color:#fff;transform:translateY(-1px);box-shadow:0 4px 12px rgba(0,102,255,.3)}",
112
+ ".cm-plu-open-btn:active{transform:scale(0.97)}"
113
+ ].join("\n");
114
+ };
115
+
116
+ PluCssBuilder.prototype.buildMobileOverrides = function() {
117
+ return "@media(max-width:600px){.cm-plu-scroll{padding:16px 12px}.cm-plu-zone-card{padding:12px 14px}}";
118
+ };
119
+
120
+ function BanAutocompleteManager(inputEl, suggestionsEl) {
121
+ this.inputEl = inputEl;
122
+ this.suggestionsEl = suggestionsEl;
123
+ this.debounceTimer = null;
124
+ this.highlightedIndex = -1;
125
+ this.results = [];
126
+ this.selectedAddress = null;
127
+ this.onSelect = null;
128
+ this.bindEvents();
129
+ }
130
+
131
+ BanAutocompleteManager.prototype.bindEvents = function() {
132
+ var self = this;
133
+ this.inputEl.addEventListener("input", function() {
134
+ self.selectedAddress = null;
135
+ self.scheduleSearch(self.inputEl.value.trim());
136
+ });
137
+ this.inputEl.addEventListener("keydown", function(e) {
138
+ if (!self.results.length) return;
139
+ if (e.key === "ArrowDown") {
140
+ e.preventDefault();
141
+ self.highlightedIndex = Math.min(self.highlightedIndex + 1, self.results.length - 1);
142
+ self.renderHighlight();
143
+ } else if (e.key === "ArrowUp") {
144
+ e.preventDefault();
145
+ self.highlightedIndex = Math.max(self.highlightedIndex - 1, 0);
146
+ self.renderHighlight();
147
+ } else if (e.key === "Enter") {
148
+ e.preventDefault();
149
+ if (self.highlightedIndex >= 0) self.selectResult(self.results[self.highlightedIndex]);
150
+ } else if (e.key === "Escape") {
151
+ self.hideSuggestions();
152
+ }
153
+ });
154
+ document.addEventListener("click", function(e) {
155
+ if (!self.inputEl.contains(e.target) && !self.suggestionsEl.contains(e.target)) self.hideSuggestions();
156
+ });
157
+ };
158
+
159
+ BanAutocompleteManager.prototype.scheduleSearch = function(query) {
160
+ var self = this;
161
+ clearTimeout(this.debounceTimer);
162
+ if (query.length < 3) { this.hideSuggestions(); return; }
163
+ this.debounceTimer = setTimeout(function() { self.search(query); }, BAN_DEBOUNCE_MS);
164
+ };
165
+
166
+ BanAutocompleteManager.prototype.search = async function(query) {
167
+ var url = BAN_API_URL + "?q=" + encodeURIComponent(query) + "&limit=" + BAN_MAX_RESULTS;
168
+ var response = await fetch(url);
169
+ if (!response.ok) throw new Error("BAN API erreur HTTP " + response.status);
170
+ var data = await response.json();
171
+ this.results = (data.features || []).map(function(f) {
172
+ var props = f.properties || {};
173
+ var coords = f.geometry && f.geometry.coordinates ? f.geometry.coordinates : [0, 0];
174
+ return { label: props.label || "", city: props.city || "", postcode: props.postcode || "", citycode: props.citycode || "", longitude: coords[0], latitude: coords[1] };
175
+ });
176
+ this.highlightedIndex = -1;
177
+ this.renderSuggestions();
178
+ };
179
+
180
+ BanAutocompleteManager.prototype.renderSuggestions = function() {
181
+ var self = this;
182
+ if (!this.results.length) { this.hideSuggestions(); return; }
183
+ this.suggestionsEl.innerHTML = this.results.map(function(r, idx) {
184
+ return '<div class="cm-plu-suggestion" data-idx="' + idx + '">' + self.escapeHtml(r.label) + '</div>';
185
+ }).join("");
186
+ this.suggestionsEl.classList.add("visible");
187
+ this.suggestionsEl.querySelectorAll(".cm-plu-suggestion").forEach(function(el) {
188
+ el.addEventListener("click", function() { self.selectResult(self.results[Number(el.getAttribute("data-idx"))]); });
189
+ });
190
+ };
191
+
192
+ BanAutocompleteManager.prototype.renderHighlight = function() {
193
+ var self = this;
194
+ this.suggestionsEl.querySelectorAll(".cm-plu-suggestion").forEach(function(el, idx) {
195
+ el.classList.toggle("highlighted", idx === self.highlightedIndex);
196
+ });
197
+ };
198
+
199
+ BanAutocompleteManager.prototype.selectResult = function(result) {
200
+ if (!result) return;
201
+ this.selectedAddress = result;
202
+ this.inputEl.value = result.label;
203
+ this.hideSuggestions();
204
+ if (typeof this.onSelect === "function") this.onSelect(result);
205
+ };
206
+
207
+ BanAutocompleteManager.prototype.hideSuggestions = function() {
208
+ this.suggestionsEl.classList.remove("visible");
209
+ this.results = [];
210
+ this.highlightedIndex = -1;
211
+ };
212
+
213
+ BanAutocompleteManager.prototype.escapeHtml = function(str) {
214
+ var div = document.createElement("div");
215
+ div.textContent = str;
216
+ return div.innerHTML;
217
+ };
218
+
219
+ BanAutocompleteManager.prototype.reset = function() {
220
+ this.inputEl.value = "";
221
+ this.selectedAddress = null;
222
+ this.hideSuggestions();
223
+ };
224
+
225
+ var ICON_HOUSE = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><path d="M3 9l9-7 9 7v11a2 2 0 01-2 2H5a2 2 0 01-2-2z"/><polyline points="9 22 9 12 15 12 15 22"/></svg>';
226
+ var ICON_PIN = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0118 0z"/><circle cx="12" cy="10" r="3"/></svg>';
227
+ var ICON_SEARCH = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>';
228
+ var ICON_WARN = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 9v4"/><path d="M12 17h.01"/><path d="M10.29 3.86L1.82 18a2 2 0 001.71 3h16.94a2 2 0 001.71-3L13.71 3.86a2 2 0 00-3.42 0z"/></svg>';
229
+ var ICON_X = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>';
230
+
231
+ function PluModuleManager(rootEl, events, apiManager, uiManager) {
232
+ this.root = rootEl;
233
+ this.events = events;
234
+ this.api = apiManager;
235
+ this.ui = uiManager;
236
+ this.autocomplete = null;
237
+ this.contentEl = null;
238
+ this.stateInputEl = null;
239
+ this.stateLoadingEl = null;
240
+ this.stateResultEl = null;
241
+ this.stateErrorEl = null;
242
+ this.searchBtnEl = null;
243
+ this.mounted = false;
244
+ }
245
+
246
+ PluModuleManager.prototype.injectCSS = function() {
247
+ var styleEl = document.createElement("style");
248
+ styleEl.id = "cm-plu-css";
249
+ styleEl.textContent = new PluCssBuilder().build();
250
+ this.root.appendChild(styleEl);
251
+ };
252
+
253
+ PluModuleManager.prototype.mount = function() {
254
+ if (this.mounted) return;
255
+ this.injectCSS();
256
+ var tabsBar = this.root.querySelector("#cm-tabs-bar");
257
+ if (tabsBar) {
258
+ var pluTab = document.createElement("button");
259
+ pluTab.className = "cm-tab hidden";
260
+ pluTab.setAttribute("data-tab", "plu");
261
+ pluTab.type = "button";
262
+ pluTab.innerHTML = ICON_HOUSE + 'Urbanisme<button class="cm-tab-close" type="button" aria-label="Fermer l\'onglet urbanisme">' + ICON_X + '</button>';
263
+ tabsBar.appendChild(pluTab);
264
+ var self = this;
265
+ pluTab.addEventListener("click", function() {
266
+ if (!pluTab.classList.contains("hidden")) self.switchTab("plu");
267
+ });
268
+ var closeBtn = pluTab.querySelector(".cm-tab-close");
269
+ if (closeBtn) closeBtn.addEventListener("click", function(e) { e.stopPropagation(); self.close(); });
270
+ }
271
+ var content = document.createElement("div");
272
+ content.id = "cm-plu-content";
273
+ content.innerHTML = this.buildHTML();
274
+ var panel = this.ui.getPanel();
275
+ var formEl = panel.querySelector("#cm-form");
276
+ if (formEl) { panel.insertBefore(content, formEl); } else { panel.appendChild(content); }
277
+ this.contentEl = content;
278
+ this.stateInputEl = content.querySelector(".cm-plu-state-input");
279
+ this.stateLoadingEl = content.querySelector(".cm-plu-loading");
280
+ this.stateResultEl = content.querySelector(".cm-plu-result");
281
+ this.stateErrorEl = content.querySelector(".cm-plu-error");
282
+ this.searchBtnEl = content.querySelector(".cm-plu-search-btn");
283
+ this.autocomplete = new BanAutocompleteManager(content.querySelector(".cm-plu-input"), content.querySelector(".cm-plu-suggestions"));
284
+ this.bindEvents();
285
+ this.mounted = true;
286
+ };
287
+
288
+ PluModuleManager.prototype.buildHTML = function() {
289
+ return [
290
+ '<div class="cm-plu-scroll">',
291
+ '<div class="cm-plu-state-input" style="display:flex;flex-direction:column;gap:16px">',
292
+ '<h3 class="cm-plu-title">' + ICON_HOUSE + ' Recherche PLU</h3>',
293
+ '<p class="cm-plu-desc">Saisissez votre adresse pour conna\u00eetre les r\u00e8gles d\'urbanisme applicables \u00e0 votre parcelle.</p>',
294
+ '<div class="cm-plu-input-wrap"><input class="cm-plu-input" type="text" placeholder="Tapez votre adresse\u2026" autocomplete="off"/><div class="cm-plu-suggestions"></div></div>',
295
+ '<button class="cm-plu-search-btn" type="button" disabled>' + ICON_SEARCH + ' Rechercher</button>',
296
+ '</div>',
297
+ '<div class="cm-plu-loading"><div class="cm-plu-spinner"></div><div class="cm-plu-loading-text">Recherche en cours\u2026</div><div class="cm-plu-loading-sub">Identification de votre parcelle et r\u00e9cup\u00e9ration des r\u00e8gles PLU\u2026</div></div>',
298
+ '<div class="cm-plu-result"></div>',
299
+ '<div class="cm-plu-error"></div>',
300
+ '</div>'
301
+ ].join("");
302
+ };
303
+
304
+ PluModuleManager.prototype.bindEvents = function() {
305
+ var self = this;
306
+ this.autocomplete.onSelect = function() { self.searchBtnEl.disabled = false; };
307
+ this.searchBtnEl.addEventListener("click", function() { self.submitQuery(); });
308
+ };
309
+
310
+ PluModuleManager.prototype.switchTab = function(tabName) {
311
+ var isPlu = tabName === "plu";
312
+ var tabsBar = this.root.querySelector("#cm-tabs-bar");
313
+ if (tabsBar) {
314
+ tabsBar.querySelectorAll(".cm-tab").forEach(function(t) {
315
+ t.classList.toggle("active", t.getAttribute("data-tab") === tabName);
316
+ });
317
+ }
318
+ this.root.querySelectorAll("#cm-messages, #cm-typing, #cm-error, #cm-disclaimer, #cm-transparency").forEach(function(el) { el.style.display = isPlu ? "none" : ""; });
319
+ var sigContent = this.root.querySelector("#cm-signalement-content");
320
+ if (sigContent) { sigContent.style.display = "none"; sigContent.classList.remove("active"); }
321
+ var sigFooter = this.root.querySelector(".cm-sig-footer");
322
+ if (sigFooter) sigFooter.classList.remove("active");
323
+ var chatForm = this.root.querySelector("#cm-form");
324
+ if (chatForm) chatForm.style.display = isPlu ? "none" : "";
325
+ this.contentEl.classList.toggle("active", isPlu);
326
+ this.contentEl.style.display = isPlu ? "flex" : "none";
327
+ };
328
+
329
+ PluModuleManager.prototype.open = function() {
330
+ var tabsBar = this.root.querySelector("#cm-tabs-bar");
331
+ if (tabsBar) {
332
+ var pluTab = tabsBar.querySelector('[data-tab="plu"]');
333
+ if (pluTab) pluTab.classList.remove("hidden");
334
+ tabsBar.classList.add("visible");
335
+ }
336
+ this.switchTab("plu");
337
+ this.showState("input");
338
+ this.events.emit("module:plu:open", {});
339
+ };
340
+
341
+ PluModuleManager.prototype.close = function() {
342
+ var tabsBar = this.root.querySelector("#cm-tabs-bar");
343
+ if (tabsBar) {
344
+ var pluTab = tabsBar.querySelector('[data-tab="plu"]');
345
+ if (pluTab) pluTab.classList.add("hidden");
346
+ }
347
+ this.switchTab("chat");
348
+ this.events.emit("module:plu:closed", {});
349
+ };
350
+
351
+ PluModuleManager.prototype.showState = function(state) {
352
+ this.stateInputEl.style.display = state === "input" ? "flex" : "none";
353
+ this.stateLoadingEl.classList.toggle("visible", state === "loading");
354
+ this.stateResultEl.classList.toggle("visible", state === "result");
355
+ this.stateErrorEl.classList.toggle("visible", state === "error");
356
+ };
357
+
358
+ PluModuleManager.prototype.submitQuery = async function() {
359
+ var address = this.autocomplete.selectedAddress;
360
+ if (!address) return;
361
+ this.showState("loading");
362
+ this.events.emit("module:plu:submitted", address);
363
+ try {
364
+ await this.api.ensureSession();
365
+ var url = this.api.resolveRuntimeUrl("/v1/modules/plu/query");
366
+ var headers = this.api.buildHeaders({ "Content-Type": "application/json" });
367
+ var body = JSON.stringify({
368
+ session_id: this.api.state.sessionId || "",
369
+ address_label: address.label,
370
+ latitude: address.latitude,
371
+ longitude: address.longitude,
372
+ city_insee_code: address.citycode
373
+ });
374
+ var response = await fetch(url, { method: "POST", headers: headers, body: body });
375
+ if (!response.ok) {
376
+ var errText = await response.text().catch(function() { return ""; });
377
+ var errObj = {};
378
+ try { errObj = JSON.parse(errText); } catch (_) {}
379
+ throw { message: errObj.message || errObj.error || "Erreur HTTP " + response.status, code: errObj.error_code || "PLU_HTTP_" + response.status };
380
+ }
381
+ var result = await response.json();
382
+ this.showResult(address, result);
383
+ this.events.emit("module:plu:resolved", result);
384
+ } catch (error) {
385
+ this.showError(
386
+ (error && error.message) || "Impossible de résoudre le PLU pour cette adresse.",
387
+ (error && error.code) || "PLU_UNKNOWN_ERROR"
388
+ );
389
+ this.events.emit("module:plu:error", { message: error && error.message, code: error && error.code });
390
+ }
391
+ };
392
+
393
+ PluModuleManager.prototype.showResult = function(address, data) {
394
+ var rulesHtml = "";
395
+ if (data.rules && data.rules.length) {
396
+ rulesHtml = '<div class="cm-plu-rules-title">Règles applicables :</div><ul class="cm-plu-rules-list">' +
397
+ data.rules.map(function(r) { return "<li>" + escapeHtml(typeof r === "string" ? r : (r.label || r.description || "")) + "</li>"; }).join("") + '</ul>';
398
+ }
399
+ var sourcesText = "";
400
+ if (data.sources && data.sources.length) {
401
+ sourcesText = "Sources : " + data.sources.map(function(s) { return escapeHtml(typeof s === "string" ? s : (s.name || "")); }).join(", ");
402
+ }
403
+ this.stateResultEl.innerHTML = [
404
+ '<h3 class="cm-plu-title">' + ICON_HOUSE + ' Résultat PLU</h3>',
405
+ '<div class="cm-plu-result-addr">' + ICON_PIN + '<div>' + escapeHtml(address.label) + '</div></div>',
406
+ '<div class="cm-plu-zone-card"><div class="cm-plu-zone-code">Zone : ' + escapeHtml(data.zone_code || "N/A") + '</div>',
407
+ '<div class="cm-plu-zone-label">' + escapeHtml(data.zone_label || "") + '</div>',
408
+ data.document_type ? '<div class="cm-plu-zone-doc">Document : ' + escapeHtml(data.document_type) + '</div>' : '',
409
+ '</div>', rulesHtml,
410
+ sourcesText ? '<div class="cm-plu-sources">' + sourcesText + '</div>' : '',
411
+ '<div class="cm-plu-disclaimer">' + ICON_WARN + '<div>' + escapeHtml(data.disclaimer || "Information indicative. Consultez le service urbanisme de votre mairie pour confirmation.") + '</div></div>',
412
+ '<button class="cm-plu-new-search-btn" type="button">' + ICON_SEARCH + ' Nouvelle recherche</button>'
413
+ ].join("");
414
+ var self = this;
415
+ var newBtn = this.stateResultEl.querySelector(".cm-plu-new-search-btn");
416
+ if (newBtn) newBtn.addEventListener("click", function() { self.resetToInput(); });
417
+ this.showState("result");
418
+ };
419
+
420
+ PluModuleManager.prototype.showError = function(message, code) {
421
+ this.stateErrorEl.innerHTML = [
422
+ '<div class="cm-plu-error-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><line x1="15" y1="9" x2="9" y2="15"/><line x1="9" y1="9" x2="15" y2="15"/></svg></div>',
423
+ '<div class="cm-plu-error-msg">' + escapeHtml(message) + '</div>',
424
+ code ? '<div class="cm-plu-error-code">' + escapeHtml(code) + '</div>' : '',
425
+ '<button class="cm-plu-retry-btn" type="button">' + ICON_SEARCH + ' Réessayer</button>'
426
+ ].join("");
427
+ var self = this;
428
+ var retryBtn = this.stateErrorEl.querySelector(".cm-plu-retry-btn");
429
+ if (retryBtn) retryBtn.addEventListener("click", function() { self.resetToInput(); });
430
+ this.showState("error");
431
+ };
432
+
433
+ PluModuleManager.prototype.resetToInput = function() {
434
+ this.autocomplete.reset();
435
+ this.searchBtnEl.disabled = true;
436
+ this.showState("input");
437
+ };
438
+
439
+ PluModuleManager.prototype.createOpenButton = function() {
440
+ var btn = document.createElement("button");
441
+ btn.className = "cm-plu-open-btn";
442
+ btn.type = "button";
443
+ btn.innerHTML = ICON_HOUSE + 'Urbanisme';
444
+ var self = this;
445
+ btn.addEventListener("click", function() { self.open(); });
446
+ return btn;
447
+ };
448
+
449
+ function escapeHtml(str) {
450
+ var div = document.createElement("div");
451
+ div.textContent = String(str || "");
452
+ return div.innerHTML;
453
+ }
454
+
455
+ runtime.PluModuleManager = PluModuleManager;
456
+ runtime.PluCssBuilder = PluCssBuilder;
457
+ runtime.BanAutocompleteManager = BanAutocompleteManager;
458
+ })();