@ourlu/assistant-sdk 0.2.0 → 0.2.3
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 +119 -72
- package/dist/esm/index.js +1 -1
- package/dist/iife/audio.v1.95146620.js +179 -0
- package/dist/iife/audio.v1.js +1 -1
- package/dist/iife/engine.v1.3b09dc20.js +721 -0
- package/dist/iife/engine.v1.773fc15d.js +645 -0
- package/dist/iife/engine.v1.js +721 -645
- package/dist/iife/loader.v1.js +22 -21
- package/dist/iife/ui.v1.41a99419.js +919 -0
- package/dist/iife/ui.v1.7417819d.js +919 -0
- package/dist/iife/ui.v1.js +6 -6
- package/dist/iife/widget-manifest.json +5 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,109 +1,156 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @ourlu/assistant-sdk
|
|
2
2
|
|
|
3
|
-
SDK JavaScript
|
|
3
|
+
SDK JavaScript pour intégrer le chatbot IA Ourlu sur les sites web de collectivités.
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## Installation
|
|
6
6
|
|
|
7
|
-
-
|
|
8
|
-
- Le SDK reste découplé de Temporal et des choix d'orchestration backend.
|
|
9
|
-
- Priorité conservée: stabilité contractuelle (`SDK_CONTRACT_V1`) et diffusion versionnée.
|
|
7
|
+
### Copier-coller (recommandé)
|
|
10
8
|
|
|
11
|
-
|
|
9
|
+
Ajoutez ce snippet avant la balise `</body>` de votre site :
|
|
12
10
|
|
|
13
|
-
|
|
11
|
+
```html
|
|
12
|
+
<script
|
|
13
|
+
src="https://occe.ourlu.fr/v1/widget/runtime/loader.v1.js"
|
|
14
|
+
data-tenant-id="votre-identifiant-mairie"
|
|
15
|
+
data-widget-key="wpk_votre_cle_publique"
|
|
16
|
+
data-api-base-url="https://occe.ourlu.fr"
|
|
17
|
+
async
|
|
18
|
+
></script>
|
|
19
|
+
```
|
|
14
20
|
|
|
15
|
-
|
|
16
|
-
- les runtimes UI/engine (`ui.v1.js`, `engine.v1.js`);
|
|
17
|
-
- une API ESM (`dist/esm/index.js`) pour bootstrap programmatique.
|
|
21
|
+
Le chatbot apparaît automatiquement en bas à droite de votre site.
|
|
18
22
|
|
|
19
|
-
|
|
23
|
+
### npm (usage programmatique)
|
|
20
24
|
|
|
21
|
-
|
|
25
|
+
```bash
|
|
26
|
+
npm i @ourlu/assistant-sdk
|
|
27
|
+
```
|
|
22
28
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
- `src/runtime/loader.runtime.engine.v1.js`: appels API chat/audio + orchestration runtime.
|
|
26
|
-
- `src/esm/index.js`: helpers publics (`bootstrapWidgetRuntime`, `mountWidgetFromScript`) et managers ESM.
|
|
27
|
-
- `scripts/build.mjs`: build par copie contrôlée `src -> dist`.
|
|
28
|
-
- `tests/runtime-url-resolver.test.mjs`: tests Node du resolver d'URLs runtime.
|
|
29
|
-
- `.github/workflows/sdk-ci-release.yml`: pipeline CI + publication sur tags `v*`.
|
|
29
|
+
```javascript
|
|
30
|
+
import { bootstrapWidgetRuntime } from '@ourlu/assistant-sdk';
|
|
30
31
|
|
|
31
|
-
|
|
32
|
+
bootstrapWidgetRuntime({
|
|
33
|
+
loaderUrl: 'https://occe.ourlu.fr/v1/widget/runtime/loader.v1.js'
|
|
34
|
+
});
|
|
35
|
+
```
|
|
32
36
|
|
|
33
|
-
|
|
37
|
+
---
|
|
34
38
|
|
|
35
|
-
|
|
36
|
-
- `mountWidgetFromScript(scriptTag)`: délègue au runtime global `window.__CompanionWidgetRuntimeV1`.
|
|
37
|
-
- `RuntimeUrlResolverManager`
|
|
38
|
-
- `BrowserScriptLoaderManager`
|
|
39
|
-
- `RuntimeBootstrapManager`
|
|
40
|
-
- `sdkVersion` (`"v1"`).
|
|
39
|
+
## Configuration
|
|
41
40
|
|
|
42
|
-
|
|
41
|
+
| Attribut | Requis | Description |
|
|
42
|
+
|----------|--------|-------------|
|
|
43
|
+
| `data-tenant-id` | ✅ | Identifiant de votre collectivité |
|
|
44
|
+
| `data-widget-key` | ✅ | Clé publique du widget (fournie dans l'espace admin) |
|
|
45
|
+
| `data-api-base-url` | ✅ | URL de la plateforme (`https://occe.ourlu.fr`) |
|
|
46
|
+
| `data-turnstile-site-key` | ❌ | Clé Cloudflare Turnstile pour protection anti-bot |
|
|
43
47
|
|
|
44
|
-
|
|
45
|
-
- `uiUrl` / `engineUrl` (optionnels) pour surcharger explicitement;
|
|
46
|
-
- `timeoutMs` (optionnel, défaut interne `8000`).
|
|
48
|
+
Le thème (couleurs, mascotte, position, textes) est géré automatiquement depuis votre espace d'administration Ourlu. Aucune modification du code d'intégration n'est nécessaire après un changement de design.
|
|
47
49
|
|
|
48
|
-
|
|
50
|
+
---
|
|
49
51
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
52
|
+
## Compatibilité
|
|
53
|
+
|
|
54
|
+
### CMS & plateformes
|
|
55
|
+
|
|
56
|
+
WordPress, Drupal, SPIP, Joomla, TYPO3, Wix, Squarespace, Webflow, Shopify, et tout site acceptant un tag `<script>`.
|
|
57
|
+
|
|
58
|
+
### Frameworks JavaScript
|
|
59
|
+
|
|
60
|
+
React, Vue, Svelte, Angular, Next.js, Nuxt, Astro, Remix, SvelteKit.
|
|
61
|
+
|
|
62
|
+
### Sites statiques
|
|
56
63
|
|
|
57
|
-
|
|
64
|
+
HTML pur, Jekyll, Hugo, Eleventy, Cloudflare Pages, Netlify, Vercel, GitHub Pages.
|
|
58
65
|
|
|
59
|
-
|
|
60
|
-
- `test`: `node --test ./tests/*.test.mjs`
|
|
61
|
-
- `prepack`: build + test
|
|
62
|
-
- `pack:check`: `npm pack --dry-run`
|
|
63
|
-
- `publish:dry-run`: `npm publish --dry-run`
|
|
66
|
+
### Navigateurs supportés
|
|
64
67
|
|
|
65
|
-
|
|
68
|
+
- Chrome 66+
|
|
69
|
+
- Firefox 60+
|
|
70
|
+
- Safari 12+
|
|
71
|
+
- Edge 79+
|
|
72
|
+
- Mobile (iOS Safari, Chrome Android)
|
|
66
73
|
|
|
67
|
-
|
|
74
|
+
---
|
|
68
75
|
|
|
69
|
-
|
|
70
|
-
- `GET /v1/widget/runtime/ui.v1.js`
|
|
71
|
-
- `GET /v1/widget/runtime/engine.v1.js`
|
|
76
|
+
## Fonctionnalités
|
|
72
77
|
|
|
73
|
-
|
|
78
|
+
- 💬 Chat textuel avec assistant IA en temps réel (streaming SSE)
|
|
79
|
+
- 🎙️ Entrée vocale avec transcription automatique
|
|
80
|
+
- 🎨 Thème personnalisable (couleurs, position, mascotte)
|
|
81
|
+
- 🌓 Mode sombre automatique (suit les préférences système)
|
|
82
|
+
- 🛡️ Protection anti-bot Cloudflare Turnstile (optionnelle)
|
|
83
|
+
- 🔒 Authentification JWT automatique
|
|
84
|
+
- ♿ Accessible (navigation clavier, échappement pour fermer)
|
|
85
|
+
- 📱 Responsive (mobile et desktop)
|
|
74
86
|
|
|
75
|
-
|
|
87
|
+
---
|
|
76
88
|
|
|
77
|
-
|
|
78
|
-
2. exécuter la synchronisation côté `ourlu`.
|
|
89
|
+
## Protection anti-bot (optionnelle)
|
|
79
90
|
|
|
80
|
-
|
|
91
|
+
Pour activer Cloudflare Turnstile :
|
|
81
92
|
|
|
82
|
-
```
|
|
83
|
-
|
|
93
|
+
```html
|
|
94
|
+
<script
|
|
95
|
+
src="https://occe.ourlu.fr/v1/widget/runtime/loader.v1.js"
|
|
96
|
+
data-tenant-id="votre-mairie"
|
|
97
|
+
data-widget-key="wpk_..."
|
|
98
|
+
data-api-base-url="https://occe.ourlu.fr"
|
|
99
|
+
data-turnstile-site-key="0x4AAAAAAA..."
|
|
100
|
+
async
|
|
101
|
+
></script>
|
|
84
102
|
```
|
|
85
103
|
|
|
86
|
-
|
|
87
|
-
|
|
104
|
+
Le captcha est invisible dans 99% des cas. Sans la clé Turnstile, le chatbot fonctionne normalement sans protection anti-bot.
|
|
105
|
+
|
|
106
|
+
---
|
|
107
|
+
|
|
108
|
+
## API publique (ESM)
|
|
109
|
+
|
|
110
|
+
Pour une intégration programmatique avancée :
|
|
111
|
+
|
|
112
|
+
```javascript
|
|
113
|
+
import {
|
|
114
|
+
bootstrapWidgetRuntime,
|
|
115
|
+
mountWidgetFromScript,
|
|
116
|
+
sdkVersion
|
|
117
|
+
} from '@ourlu/assistant-sdk';
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
| Export | Description |
|
|
121
|
+
|--------|-------------|
|
|
122
|
+
| `bootstrapWidgetRuntime(options)` | Charge et initialise le widget |
|
|
123
|
+
| `mountWidgetFromScript(scriptTag)` | Monte le widget depuis un élément script existant |
|
|
124
|
+
| `sdkVersion` | Version du SDK (`"v1"`) |
|
|
125
|
+
|
|
126
|
+
### Options `bootstrapWidgetRuntime`
|
|
127
|
+
|
|
128
|
+
| Option | Description | Défaut |
|
|
129
|
+
|--------|-------------|--------|
|
|
130
|
+
| `loaderUrl` | URL du loader (résout automatiquement UI + engine) | — |
|
|
131
|
+
| `uiUrl` | URL explicite du runtime UI | Auto |
|
|
132
|
+
| `engineUrl` | URL explicite du runtime engine | Auto |
|
|
133
|
+
| `timeoutMs` | Timeout de chargement en ms | `8000` |
|
|
88
134
|
|
|
89
|
-
|
|
135
|
+
---
|
|
90
136
|
|
|
91
|
-
|
|
137
|
+
## Sécurité & RGPD
|
|
92
138
|
|
|
93
|
-
-
|
|
94
|
-
-
|
|
139
|
+
- Aucun cookie tiers installé
|
|
140
|
+
- Anonymisation native des données personnelles (PII masking)
|
|
141
|
+
- Compatible Content Security Policy (CSP) stricte
|
|
142
|
+
- Option auto-hébergement des fichiers JS
|
|
143
|
+
- Option reverse-proxy pour anonymisation IP
|
|
95
144
|
|
|
96
|
-
|
|
145
|
+
---
|
|
97
146
|
|
|
98
|
-
|
|
99
|
-
- changelog: `CHANGELOG.md`
|
|
100
|
-
- stratégie de release: `docs/RELEASE_STRATEGY.md`
|
|
147
|
+
## Support
|
|
101
148
|
|
|
102
|
-
|
|
149
|
+
- Documentation : espace admin Ourlu > Intégration
|
|
150
|
+
- Contact technique : integration@ourlu.fr
|
|
103
151
|
|
|
104
|
-
|
|
152
|
+
---
|
|
105
153
|
|
|
106
|
-
|
|
154
|
+
## Licence
|
|
107
155
|
|
|
108
|
-
|
|
109
|
-
- job `publish` sur tag `v*`: `npm publish --access restricted` avec `NPM_TOKEN`.
|
|
156
|
+
Propriétaire — Ourlu SAS. Usage réservé aux collectivités clientes.
|
package/dist/esm/index.js
CHANGED
|
@@ -69,7 +69,7 @@ class RuntimeBootstrapManager {
|
|
|
69
69
|
}
|
|
70
70
|
|
|
71
71
|
mountFromScript(scriptTag) {
|
|
72
|
-
const runtime = window.
|
|
72
|
+
const runtime = window.__OurluWidgetRuntimeV1;
|
|
73
73
|
if (!runtime || typeof runtime.mountFromScript !== "function") {
|
|
74
74
|
throw new Error("Entrée runtime SDK absente.");
|
|
75
75
|
}
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
(function() {
|
|
2
|
+
"use strict";
|
|
3
|
+
var runtime = window.__OurluWidgetRuntimeV1 || (window.__OurluWidgetRuntimeV1 = {});
|
|
4
|
+
if (!runtime.utils) {
|
|
5
|
+
throw new Error("Widget runtime utils module must be loaded first.");
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
var mergeTranscript = runtime.utils.mergeTranscript;
|
|
9
|
+
var resolveRecorderMimeType = runtime.utils.resolveRecorderMimeType;
|
|
10
|
+
var encodeArrayBufferToBase64 = runtime.utils.encodeArrayBufferToBase64;
|
|
11
|
+
|
|
12
|
+
function WidgetAudioManager(state, api, ui) {
|
|
13
|
+
this.state = state;
|
|
14
|
+
this.api = api;
|
|
15
|
+
this.ui = ui;
|
|
16
|
+
this.stream = null;
|
|
17
|
+
this.recorder = null;
|
|
18
|
+
this.chunks = [];
|
|
19
|
+
this.stopDraftStream = null;
|
|
20
|
+
this.activeAudioSessionId = "";
|
|
21
|
+
this.cleanupTimer = null;
|
|
22
|
+
this._starting = false;
|
|
23
|
+
this._stopping = false;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
WidgetAudioManager.prototype.isSupported = function() {
|
|
27
|
+
return !!(navigator.mediaDevices && navigator.mediaDevices.getUserMedia && typeof MediaRecorder !== "undefined");
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
WidgetAudioManager.prototype.toggle = async function() {
|
|
31
|
+
if (this._starting || this._stopping) return;
|
|
32
|
+
if (this.state.listening) return this.stop();
|
|
33
|
+
return this.start();
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
WidgetAudioManager.prototype.start = async function() {
|
|
37
|
+
if (!this.isSupported() || this.state.sending || this._starting || this._stopping) return;
|
|
38
|
+
this._starting = true;
|
|
39
|
+
this.ui.setMicListening(true);
|
|
40
|
+
try {
|
|
41
|
+
this.ui.showError("");
|
|
42
|
+
this.clearDraftCleanup();
|
|
43
|
+
if (this.activeAudioSessionId && this.state.sessionId) {
|
|
44
|
+
try {
|
|
45
|
+
await this.api.closeAudioSession(this.state.sessionId, this.activeAudioSessionId);
|
|
46
|
+
} catch (_) { /* best-effort */ }
|
|
47
|
+
this.activeAudioSessionId = "";
|
|
48
|
+
}
|
|
49
|
+
var sessionId = await this.api.ensureSession();
|
|
50
|
+
if (this.stopDraftStream) this.stopDraftStream();
|
|
51
|
+
this.stopDraftStream = await this.api.streamAudioDraft(sessionId, {
|
|
52
|
+
onDelta: this.handleDraftDelta.bind(this),
|
|
53
|
+
onComplete: this.handleDraftComplete.bind(this),
|
|
54
|
+
onError: this.handleDraftError.bind(this),
|
|
55
|
+
onTransportError: this.handleDraftTransportError.bind(this)
|
|
56
|
+
});
|
|
57
|
+
var audioSession = await this.api.startAudioSession(sessionId);
|
|
58
|
+
this.activeAudioSessionId = audioSession.audio_session_id || "";
|
|
59
|
+
this.stream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
|
60
|
+
this.chunks = [];
|
|
61
|
+
var mimeType = resolveRecorderMimeType();
|
|
62
|
+
this.recorder = new MediaRecorder(this.stream, mimeType ? { mimeType: mimeType } : {});
|
|
63
|
+
var self = this;
|
|
64
|
+
this.recorder.ondataavailable = function(event) {
|
|
65
|
+
if (event.data && event.data.size > 0) self.chunks.push(event.data);
|
|
66
|
+
};
|
|
67
|
+
this.recorder.start(250);
|
|
68
|
+
this.state.listening = true;
|
|
69
|
+
} catch (error) {
|
|
70
|
+
this.cleanupMedia();
|
|
71
|
+
this.activeAudioSessionId = "";
|
|
72
|
+
this.state.listening = false;
|
|
73
|
+
this.ui.setMicListening(false);
|
|
74
|
+
this.ui.showError("Erreur démarrage audio : " + (error.message || error));
|
|
75
|
+
} finally {
|
|
76
|
+
this._starting = false;
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
WidgetAudioManager.prototype.stop = async function() {
|
|
81
|
+
if (!this.state.listening || this._stopping) return;
|
|
82
|
+
this._stopping = true;
|
|
83
|
+
this.state.listening = false;
|
|
84
|
+
this.ui.setMicListening(false);
|
|
85
|
+
var sessionId = this.state.sessionId;
|
|
86
|
+
var audioSessionId = this.activeAudioSessionId;
|
|
87
|
+
try {
|
|
88
|
+
var blob = await this.stopRecorder();
|
|
89
|
+
if (blob && blob.size > 0 && sessionId && audioSessionId) {
|
|
90
|
+
var base64 = encodeArrayBufferToBase64(await blob.arrayBuffer());
|
|
91
|
+
await this.api.sendAudioChunk(sessionId, audioSessionId, base64);
|
|
92
|
+
}
|
|
93
|
+
if (sessionId && audioSessionId) await this.api.closeAudioSession(sessionId, audioSessionId);
|
|
94
|
+
} catch (error) {
|
|
95
|
+
this.ui.showError("Erreur fermeture audio : " + (error.message || error));
|
|
96
|
+
} finally {
|
|
97
|
+
this.activeAudioSessionId = "";
|
|
98
|
+
this._stopping = false;
|
|
99
|
+
this.scheduleDraftCleanup();
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
WidgetAudioManager.prototype.stopRecorder = function() {
|
|
104
|
+
var self = this;
|
|
105
|
+
if (!this.recorder) {
|
|
106
|
+
this.cleanupMedia();
|
|
107
|
+
return Promise.resolve(new Blob());
|
|
108
|
+
}
|
|
109
|
+
return new Promise(function(resolve) {
|
|
110
|
+
var recorder = self.recorder;
|
|
111
|
+
function done() {
|
|
112
|
+
var blob = new Blob(self.chunks, { type: recorder.mimeType || "audio/webm" });
|
|
113
|
+
self.cleanupMedia();
|
|
114
|
+
resolve(blob);
|
|
115
|
+
}
|
|
116
|
+
if (recorder.state === "inactive") return done();
|
|
117
|
+
recorder.addEventListener("stop", done, { once: true });
|
|
118
|
+
recorder.stop();
|
|
119
|
+
});
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
WidgetAudioManager.prototype.cleanupMedia = function() {
|
|
123
|
+
if (this.recorder) {
|
|
124
|
+
this.recorder.ondataavailable = null;
|
|
125
|
+
this.recorder = null;
|
|
126
|
+
}
|
|
127
|
+
if (this.stream) {
|
|
128
|
+
this.stream.getTracks().forEach(function(track) { track.stop(); });
|
|
129
|
+
this.stream = null;
|
|
130
|
+
}
|
|
131
|
+
this.chunks = [];
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
WidgetAudioManager.prototype.handleDraftDelta = function(payload) {
|
|
135
|
+
if (!payload || !payload.delta) return;
|
|
136
|
+
this.ui.setInput(mergeTranscript(this.ui.inputValue(), payload.delta));
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
WidgetAudioManager.prototype.handleDraftComplete = function(payload) {
|
|
140
|
+
if (payload && payload.text) this.ui.setInput(String(payload.text).trim());
|
|
141
|
+
this.scheduleDraftCleanup();
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
WidgetAudioManager.prototype.handleDraftError = function(payload) {
|
|
145
|
+
this.ui.showError((payload && payload.message) || "Erreur transcription audio");
|
|
146
|
+
this.scheduleDraftCleanup();
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
WidgetAudioManager.prototype.isExpectedStreamAbortMessage = function(message) {
|
|
150
|
+
var normalized = String(message || "").toLowerCase();
|
|
151
|
+
return normalized.indexOf("aborted") !== -1 ||
|
|
152
|
+
normalized.indexOf("aborterror") !== -1 ||
|
|
153
|
+
normalized.indexOf("body stream buffer was aborted") !== -1 ||
|
|
154
|
+
normalized.indexOf("the operation was aborted") !== -1;
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
WidgetAudioManager.prototype.handleDraftTransportError = function(message) {
|
|
158
|
+
if (this.isExpectedStreamAbortMessage(message)) return;
|
|
159
|
+
this.ui.showError(message || "Erreur stream audio");
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
WidgetAudioManager.prototype.clearDraftCleanup = function() {
|
|
163
|
+
if (!this.cleanupTimer) return;
|
|
164
|
+
clearTimeout(this.cleanupTimer);
|
|
165
|
+
this.cleanupTimer = null;
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
WidgetAudioManager.prototype.scheduleDraftCleanup = function() {
|
|
169
|
+
var self = this;
|
|
170
|
+
this.clearDraftCleanup();
|
|
171
|
+
this.cleanupTimer = setTimeout(function() {
|
|
172
|
+
if (self.stopDraftStream) self.stopDraftStream();
|
|
173
|
+
self.stopDraftStream = null;
|
|
174
|
+
self.cleanupTimer = null;
|
|
175
|
+
}, 3000);
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
runtime.WidgetAudioManager = WidgetAudioManager;
|
|
179
|
+
})();
|
package/dist/iife/audio.v1.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
(function() {
|
|
2
2
|
"use strict";
|
|
3
|
-
var runtime = window.
|
|
3
|
+
var runtime = window.__OurluWidgetRuntimeV1 || (window.__OurluWidgetRuntimeV1 = {});
|
|
4
4
|
if (!runtime.utils) {
|
|
5
5
|
throw new Error("Widget runtime utils module must be loaded first.");
|
|
6
6
|
}
|