@smooai/chat-widget 0.13.0 → 0.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chat-widget.global.js +472 -39
- package/dist/chat-widget.global.js.map +1 -1
- package/dist/index.d.ts +197 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +499 -40
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/config.ts +18 -1
- package/src/conversation.ts +23 -1
- package/src/element.ts +114 -1
- package/src/index.ts +19 -1
- package/src/styles.ts +41 -0
- package/src/voice-session.ts +420 -0
|
@@ -11741,6 +11741,314 @@ var SmoothAgentChat = (function(exports) {
|
|
|
11741
11741
|
return s;
|
|
11742
11742
|
}
|
|
11743
11743
|
//#endregion
|
|
11744
|
+
//#region \0@oxc-project+runtime@0.134.0/helpers/esm/typeof.js
|
|
11745
|
+
function _typeof(o) {
|
|
11746
|
+
"@babel/helpers - typeof";
|
|
11747
|
+
return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function(o) {
|
|
11748
|
+
return typeof o;
|
|
11749
|
+
} : function(o) {
|
|
11750
|
+
return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o;
|
|
11751
|
+
}, _typeof(o);
|
|
11752
|
+
}
|
|
11753
|
+
//#endregion
|
|
11754
|
+
//#region \0@oxc-project+runtime@0.134.0/helpers/esm/toPrimitive.js
|
|
11755
|
+
function toPrimitive(t, r) {
|
|
11756
|
+
if ("object" != _typeof(t) || !t) return t;
|
|
11757
|
+
var e = t[Symbol.toPrimitive];
|
|
11758
|
+
if (void 0 !== e) {
|
|
11759
|
+
var i = e.call(t, r || "default");
|
|
11760
|
+
if ("object" != _typeof(i)) return i;
|
|
11761
|
+
throw new TypeError("@@toPrimitive must return a primitive value.");
|
|
11762
|
+
}
|
|
11763
|
+
return ("string" === r ? String : Number)(t);
|
|
11764
|
+
}
|
|
11765
|
+
//#endregion
|
|
11766
|
+
//#region \0@oxc-project+runtime@0.134.0/helpers/esm/toPropertyKey.js
|
|
11767
|
+
function toPropertyKey(t) {
|
|
11768
|
+
var i = toPrimitive(t, "string");
|
|
11769
|
+
return "symbol" == _typeof(i) ? i : i + "";
|
|
11770
|
+
}
|
|
11771
|
+
//#endregion
|
|
11772
|
+
//#region \0@oxc-project+runtime@0.134.0/helpers/esm/defineProperty.js
|
|
11773
|
+
function _defineProperty(e, r, t) {
|
|
11774
|
+
return (r = toPropertyKey(r)) in e ? Object.defineProperty(e, r, {
|
|
11775
|
+
value: t,
|
|
11776
|
+
enumerable: !0,
|
|
11777
|
+
configurable: !0,
|
|
11778
|
+
writable: !0
|
|
11779
|
+
}) : e[r] = t, e;
|
|
11780
|
+
}
|
|
11781
|
+
/** Target wire format: PCM linear16 mono @ 16 kHz. */
|
|
11782
|
+
const VOICE_SAMPLE_RATE = 16e3;
|
|
11783
|
+
/**
|
|
11784
|
+
* Downsample Float32 mic samples at `inputRate` to 16 kHz mono Int16 (linear16).
|
|
11785
|
+
* Bucket-averaging decimation: each output sample averages its source bucket,
|
|
11786
|
+
* which is a cheap low-pass that's plenty for speech (and handles non-integer
|
|
11787
|
+
* ratios like 44.1k → 16k). Values are clamped to the Int16 range.
|
|
11788
|
+
*/
|
|
11789
|
+
function downsampleTo16k(samples, inputRate) {
|
|
11790
|
+
const ratio = inputRate / VOICE_SAMPLE_RATE;
|
|
11791
|
+
const outLen = ratio <= 1 ? samples.length : Math.floor(samples.length / ratio);
|
|
11792
|
+
const out = new Int16Array(outLen);
|
|
11793
|
+
for (let i = 0; i < outLen; i++) {
|
|
11794
|
+
let v;
|
|
11795
|
+
if (ratio <= 1) v = samples[i];
|
|
11796
|
+
else {
|
|
11797
|
+
const start = Math.floor(i * ratio);
|
|
11798
|
+
const end = Math.min(samples.length, Math.max(start + 1, Math.floor((i + 1) * ratio)));
|
|
11799
|
+
let sum = 0;
|
|
11800
|
+
for (let j = start; j < end; j++) sum += samples[j];
|
|
11801
|
+
v = sum / (end - start);
|
|
11802
|
+
}
|
|
11803
|
+
out[i] = Math.max(-32768, Math.min(32767, Math.round(v * 32767)));
|
|
11804
|
+
}
|
|
11805
|
+
return out;
|
|
11806
|
+
}
|
|
11807
|
+
/** Root-mean-square level of a Float32 frame (0..1) — the barge-in speech gate. */
|
|
11808
|
+
function rmsLevel(samples) {
|
|
11809
|
+
if (samples.length === 0) return 0;
|
|
11810
|
+
let sum = 0;
|
|
11811
|
+
for (let i = 0; i < samples.length; i++) sum += samples[i] * samples[i];
|
|
11812
|
+
return Math.sqrt(sum / samples.length);
|
|
11813
|
+
}
|
|
11814
|
+
/**
|
|
11815
|
+
* Gapless PCM playback: each incoming linear16 chunk becomes an AudioBuffer
|
|
11816
|
+
* scheduled back-to-back (`nextTime`) so consecutive chunks butt together with
|
|
11817
|
+
* no gaps. `flush()` stops every scheduled source (barge-in / mic-button stop).
|
|
11818
|
+
*/
|
|
11819
|
+
var PcmPlayer = class {
|
|
11820
|
+
constructor(ctx) {
|
|
11821
|
+
this.ctx = ctx;
|
|
11822
|
+
_defineProperty(this, "nextTime", 0);
|
|
11823
|
+
_defineProperty(this, "live", /* @__PURE__ */ new Set());
|
|
11824
|
+
}
|
|
11825
|
+
enqueue(chunk) {
|
|
11826
|
+
const int16 = new Int16Array(chunk);
|
|
11827
|
+
if (int16.length === 0) return;
|
|
11828
|
+
const buf = this.ctx.createBuffer(1, int16.length, VOICE_SAMPLE_RATE);
|
|
11829
|
+
const ch = buf.getChannelData(0);
|
|
11830
|
+
for (let i = 0; i < int16.length; i++) ch[i] = int16[i] / 32768;
|
|
11831
|
+
const src = this.ctx.createBufferSource();
|
|
11832
|
+
src.buffer = buf;
|
|
11833
|
+
src.connect(this.ctx.destination);
|
|
11834
|
+
const start = Math.max(this.ctx.currentTime, this.nextTime);
|
|
11835
|
+
src.start(start);
|
|
11836
|
+
this.nextTime = start + int16.length / VOICE_SAMPLE_RATE;
|
|
11837
|
+
this.live.add(src);
|
|
11838
|
+
src.onended = () => this.live.delete(src);
|
|
11839
|
+
}
|
|
11840
|
+
flush() {
|
|
11841
|
+
for (const src of this.live) try {
|
|
11842
|
+
src.stop();
|
|
11843
|
+
} catch {}
|
|
11844
|
+
this.live.clear();
|
|
11845
|
+
this.nextTime = 0;
|
|
11846
|
+
}
|
|
11847
|
+
close() {
|
|
11848
|
+
this.flush();
|
|
11849
|
+
this.ctx.close?.();
|
|
11850
|
+
}
|
|
11851
|
+
};
|
|
11852
|
+
/** AudioWorklet processor source — posts each 128-sample mic block to the main thread. */
|
|
11853
|
+
const CAPTURE_WORKLET_SRC = `
|
|
11854
|
+
registerProcessor('sac-mic-capture', class extends AudioWorkletProcessor {
|
|
11855
|
+
process(inputs) {
|
|
11856
|
+
const ch = inputs[0] && inputs[0][0];
|
|
11857
|
+
if (ch && ch.length > 0) {
|
|
11858
|
+
const copy = new Float32Array(ch);
|
|
11859
|
+
this.port.postMessage(copy.buffer, [copy.buffer]);
|
|
11860
|
+
}
|
|
11861
|
+
return true;
|
|
11862
|
+
}
|
|
11863
|
+
});
|
|
11864
|
+
`;
|
|
11865
|
+
/** Real mic capture: getUserMedia → AudioWorklet (or ScriptProcessor fallback). */
|
|
11866
|
+
const defaultStartCapture = async (onFrame) => {
|
|
11867
|
+
const stream = await navigator.mediaDevices.getUserMedia({ audio: {
|
|
11868
|
+
channelCount: 1,
|
|
11869
|
+
echoCancellation: true,
|
|
11870
|
+
noiseSuppression: true,
|
|
11871
|
+
autoGainControl: true
|
|
11872
|
+
} });
|
|
11873
|
+
const Ctx = globalThis.AudioContext;
|
|
11874
|
+
if (!Ctx) {
|
|
11875
|
+
stream.getTracks().forEach((t) => t.stop());
|
|
11876
|
+
throw new Error("AudioContext is not available");
|
|
11877
|
+
}
|
|
11878
|
+
const ctx = new Ctx();
|
|
11879
|
+
const sampleRate = ctx.sampleRate;
|
|
11880
|
+
const source = ctx.createMediaStreamSource(stream);
|
|
11881
|
+
let disconnectNode;
|
|
11882
|
+
if (ctx.audioWorklet && typeof AudioWorkletNode === "function") {
|
|
11883
|
+
const url = URL.createObjectURL(new Blob([CAPTURE_WORKLET_SRC], { type: "text/javascript" }));
|
|
11884
|
+
try {
|
|
11885
|
+
await ctx.audioWorklet.addModule(url);
|
|
11886
|
+
} finally {
|
|
11887
|
+
URL.revokeObjectURL(url);
|
|
11888
|
+
}
|
|
11889
|
+
const node = new AudioWorkletNode(ctx, "sac-mic-capture");
|
|
11890
|
+
node.port.onmessage = (ev) => onFrame(new Float32Array(ev.data), sampleRate);
|
|
11891
|
+
source.connect(node);
|
|
11892
|
+
disconnectNode = () => node.disconnect();
|
|
11893
|
+
} else {
|
|
11894
|
+
const node = ctx.createScriptProcessor(4096, 1, 1);
|
|
11895
|
+
node.onaudioprocess = (ev) => onFrame(new Float32Array(ev.inputBuffer.getChannelData(0)), sampleRate);
|
|
11896
|
+
source.connect(node);
|
|
11897
|
+
node.connect(ctx.destination);
|
|
11898
|
+
disconnectNode = () => node.disconnect();
|
|
11899
|
+
}
|
|
11900
|
+
return () => {
|
|
11901
|
+
disconnectNode();
|
|
11902
|
+
source.disconnect();
|
|
11903
|
+
stream.getTracks().forEach((t) => t.stop());
|
|
11904
|
+
ctx.close();
|
|
11905
|
+
};
|
|
11906
|
+
};
|
|
11907
|
+
const defaultCreatePlayer = () => {
|
|
11908
|
+
const Ctx = globalThis.AudioContext;
|
|
11909
|
+
if (!Ctx) throw new Error("AudioContext is not available");
|
|
11910
|
+
return new PcmPlayer(new Ctx({ sampleRate: VOICE_SAMPLE_RATE }));
|
|
11911
|
+
};
|
|
11912
|
+
var VoiceSession = class {
|
|
11913
|
+
constructor(opts, events = {}) {
|
|
11914
|
+
_defineProperty(this, "opts", void 0);
|
|
11915
|
+
_defineProperty(this, "events", void 0);
|
|
11916
|
+
_defineProperty(this, "ws", null);
|
|
11917
|
+
_defineProperty(this, "player", null);
|
|
11918
|
+
_defineProperty(this, "stopCapture", null);
|
|
11919
|
+
_defineProperty(this, "speaking", false);
|
|
11920
|
+
_defineProperty(this, "ended", false);
|
|
11921
|
+
_defineProperty(this, "state", "idle");
|
|
11922
|
+
this.opts = opts;
|
|
11923
|
+
this.events = events;
|
|
11924
|
+
}
|
|
11925
|
+
/** Open the WS, send the start frame, and begin streaming mic audio. */
|
|
11926
|
+
async start() {
|
|
11927
|
+
if (this.state !== "idle") return;
|
|
11928
|
+
this.state = "connecting";
|
|
11929
|
+
const url = this.opts.url ?? "wss://twilio-voice.smoo.ai/browser-voice/ws";
|
|
11930
|
+
const createWs = this.opts.seams?.createWebSocket ?? ((u) => new WebSocket(u));
|
|
11931
|
+
const startCapture = this.opts.seams?.startCapture ?? defaultStartCapture;
|
|
11932
|
+
const createPlayer = this.opts.seams?.createPlayer ?? defaultCreatePlayer;
|
|
11933
|
+
this.stopCapture = await startCapture((samples, rate) => this.handleMicFrame(samples, rate));
|
|
11934
|
+
try {
|
|
11935
|
+
this.player = createPlayer();
|
|
11936
|
+
const ws = createWs(url);
|
|
11937
|
+
ws.binaryType = "arraybuffer";
|
|
11938
|
+
this.ws = ws;
|
|
11939
|
+
ws.addEventListener("open", () => {
|
|
11940
|
+
const start = {
|
|
11941
|
+
type: "start",
|
|
11942
|
+
agent_id: this.opts.agentId
|
|
11943
|
+
};
|
|
11944
|
+
if (this.opts.conversationId) start.conversation_id = this.opts.conversationId;
|
|
11945
|
+
if (this.opts.token) start.token = this.opts.token;
|
|
11946
|
+
ws.send(JSON.stringify(start));
|
|
11947
|
+
this.state = "active";
|
|
11948
|
+
});
|
|
11949
|
+
ws.addEventListener("message", (ev) => this.handleServerFrame(ev.data));
|
|
11950
|
+
ws.addEventListener("close", () => this.teardown());
|
|
11951
|
+
ws.addEventListener("error", () => {
|
|
11952
|
+
this.events.onError?.("connection_error");
|
|
11953
|
+
this.teardown();
|
|
11954
|
+
});
|
|
11955
|
+
} catch (err) {
|
|
11956
|
+
this.teardown();
|
|
11957
|
+
throw err;
|
|
11958
|
+
}
|
|
11959
|
+
}
|
|
11960
|
+
/** One raw mic frame: barge-in check, then downsample → binary frame. */
|
|
11961
|
+
handleMicFrame(samples, sampleRate) {
|
|
11962
|
+
const ws = this.ws;
|
|
11963
|
+
if (!ws || ws.readyState !== 1 || this.state !== "active") return;
|
|
11964
|
+
if (this.speaking && rmsLevel(samples) > (this.opts.bargeInThreshold ?? .02)) this.interrupt();
|
|
11965
|
+
const pcm = downsampleTo16k(samples, sampleRate);
|
|
11966
|
+
ws.send(pcm.buffer);
|
|
11967
|
+
}
|
|
11968
|
+
/** Route a server frame: binary = TTS audio, string = JSON control event. */
|
|
11969
|
+
handleServerFrame(data) {
|
|
11970
|
+
if (typeof data !== "string") {
|
|
11971
|
+
this.player?.enqueue(data);
|
|
11972
|
+
return;
|
|
11973
|
+
}
|
|
11974
|
+
let msg;
|
|
11975
|
+
try {
|
|
11976
|
+
msg = JSON.parse(data);
|
|
11977
|
+
} catch {
|
|
11978
|
+
return;
|
|
11979
|
+
}
|
|
11980
|
+
switch (msg.type) {
|
|
11981
|
+
case "transcript_partial":
|
|
11982
|
+
this.events.onTranscriptPartial?.(msg.text ?? "");
|
|
11983
|
+
break;
|
|
11984
|
+
case "transcript_final":
|
|
11985
|
+
this.events.onTranscriptFinal?.(msg.text ?? "");
|
|
11986
|
+
break;
|
|
11987
|
+
case "reply_text":
|
|
11988
|
+
this.events.onReplyText?.(msg.text ?? "");
|
|
11989
|
+
break;
|
|
11990
|
+
case "speaking_started":
|
|
11991
|
+
this.setSpeaking(true);
|
|
11992
|
+
break;
|
|
11993
|
+
case "speaking_done":
|
|
11994
|
+
this.setSpeaking(false);
|
|
11995
|
+
break;
|
|
11996
|
+
case "handoff":
|
|
11997
|
+
this.stop();
|
|
11998
|
+
break;
|
|
11999
|
+
case "error":
|
|
12000
|
+
this.events.onError?.(msg.code ?? "unknown");
|
|
12001
|
+
this.stop();
|
|
12002
|
+
break;
|
|
12003
|
+
default: break;
|
|
12004
|
+
}
|
|
12005
|
+
}
|
|
12006
|
+
setSpeaking(speaking) {
|
|
12007
|
+
if (this.speaking === speaking) return;
|
|
12008
|
+
this.speaking = speaking;
|
|
12009
|
+
this.events.onSpeaking?.(speaking);
|
|
12010
|
+
}
|
|
12011
|
+
/** True while agent TTS is playing (between speaking_started/done). */
|
|
12012
|
+
get isSpeaking() {
|
|
12013
|
+
return this.speaking;
|
|
12014
|
+
}
|
|
12015
|
+
/**
|
|
12016
|
+
* Barge in: tell the server the user interrupted and flush queued TTS so the
|
|
12017
|
+
* agent goes silent immediately. Called automatically on mic speech during
|
|
12018
|
+
* playback; the widget also calls it when the visitor hits the mic button
|
|
12019
|
+
* mid-playback.
|
|
12020
|
+
*/
|
|
12021
|
+
interrupt() {
|
|
12022
|
+
if (this.ws && this.ws.readyState === 1) this.ws.send(JSON.stringify({ type: "interrupt" }));
|
|
12023
|
+
this.player?.flush();
|
|
12024
|
+
this.setSpeaking(false);
|
|
12025
|
+
}
|
|
12026
|
+
/** Graceful end: send `stop`, then tear everything down. */
|
|
12027
|
+
stop() {
|
|
12028
|
+
if (this.ws && this.ws.readyState === 1) try {
|
|
12029
|
+
this.ws.send(JSON.stringify({ type: "stop" }));
|
|
12030
|
+
} catch {}
|
|
12031
|
+
this.teardown();
|
|
12032
|
+
}
|
|
12033
|
+
/** Idempotent teardown: capture, playback, socket, `ended` event. */
|
|
12034
|
+
teardown() {
|
|
12035
|
+
if (this.ended) return;
|
|
12036
|
+
this.ended = true;
|
|
12037
|
+
this.state = "ended";
|
|
12038
|
+
this.stopCapture?.();
|
|
12039
|
+
this.stopCapture = null;
|
|
12040
|
+
this.player?.close();
|
|
12041
|
+
this.player = null;
|
|
12042
|
+
const ws = this.ws;
|
|
12043
|
+
this.ws = null;
|
|
12044
|
+
if (ws && ws.readyState <= 1) try {
|
|
12045
|
+
ws.close(1e3, "voice ended");
|
|
12046
|
+
} catch {}
|
|
12047
|
+
this.setSpeaking(false);
|
|
12048
|
+
this.events.onEnded?.();
|
|
12049
|
+
}
|
|
12050
|
+
};
|
|
12051
|
+
//#endregion
|
|
11744
12052
|
//#region src/config.ts
|
|
11745
12053
|
/**
|
|
11746
12054
|
* Public configuration surface for the chat widget.
|
|
@@ -11783,6 +12091,10 @@ var SmoothAgentChat = (function(exports) {
|
|
|
11783
12091
|
allowChatRestore: config.allowChatRestore ?? true,
|
|
11784
12092
|
allowAnonymous: config.allowAnonymous ?? false,
|
|
11785
12093
|
showToolActivity: config.showToolActivity ?? false,
|
|
12094
|
+
voice: {
|
|
12095
|
+
enabled: config.voice?.enabled ?? false,
|
|
12096
|
+
url: config.voice?.url ?? "wss://twilio-voice.smoo.ai/browser-voice/ws"
|
|
12097
|
+
},
|
|
11786
12098
|
theme: {
|
|
11787
12099
|
text: theme.text ?? "#f8fafc",
|
|
11788
12100
|
background: theme.background ?? "#040d30",
|
|
@@ -11805,44 +12117,6 @@ var SmoothAgentChat = (function(exports) {
|
|
|
11805
12117
|
return !resolved.allowAnonymous && (resolved.requireName || resolved.requireEmail || resolved.requirePhone);
|
|
11806
12118
|
}
|
|
11807
12119
|
//#endregion
|
|
11808
|
-
//#region \0@oxc-project+runtime@0.134.0/helpers/esm/typeof.js
|
|
11809
|
-
function _typeof(o) {
|
|
11810
|
-
"@babel/helpers - typeof";
|
|
11811
|
-
return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function(o) {
|
|
11812
|
-
return typeof o;
|
|
11813
|
-
} : function(o) {
|
|
11814
|
-
return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o;
|
|
11815
|
-
}, _typeof(o);
|
|
11816
|
-
}
|
|
11817
|
-
//#endregion
|
|
11818
|
-
//#region \0@oxc-project+runtime@0.134.0/helpers/esm/toPrimitive.js
|
|
11819
|
-
function toPrimitive(t, r) {
|
|
11820
|
-
if ("object" != _typeof(t) || !t) return t;
|
|
11821
|
-
var e = t[Symbol.toPrimitive];
|
|
11822
|
-
if (void 0 !== e) {
|
|
11823
|
-
var i = e.call(t, r || "default");
|
|
11824
|
-
if ("object" != _typeof(i)) return i;
|
|
11825
|
-
throw new TypeError("@@toPrimitive must return a primitive value.");
|
|
11826
|
-
}
|
|
11827
|
-
return ("string" === r ? String : Number)(t);
|
|
11828
|
-
}
|
|
11829
|
-
//#endregion
|
|
11830
|
-
//#region \0@oxc-project+runtime@0.134.0/helpers/esm/toPropertyKey.js
|
|
11831
|
-
function toPropertyKey(t) {
|
|
11832
|
-
var i = toPrimitive(t, "string");
|
|
11833
|
-
return "symbol" == _typeof(i) ? i : i + "";
|
|
11834
|
-
}
|
|
11835
|
-
//#endregion
|
|
11836
|
-
//#region \0@oxc-project+runtime@0.134.0/helpers/esm/defineProperty.js
|
|
11837
|
-
function _defineProperty(e, r, t) {
|
|
11838
|
-
return (r = toPropertyKey(r)) in e ? Object.defineProperty(e, r, {
|
|
11839
|
-
value: t,
|
|
11840
|
-
enumerable: !0,
|
|
11841
|
-
configurable: !0,
|
|
11842
|
-
writable: !0
|
|
11843
|
-
}) : e[r] = t, e;
|
|
11844
|
-
}
|
|
11845
|
-
//#endregion
|
|
11846
12120
|
//#region node_modules/.pnpm/@smooai+smooth-operator@1.21.1/node_modules/@smooai/smooth-operator/dist/transport.js
|
|
11847
12121
|
/**
|
|
11848
12122
|
* Transport abstraction for the client.
|
|
@@ -13004,6 +13278,7 @@ var SmoothAgentChat = (function(exports) {
|
|
|
13004
13278
|
_defineProperty(this, "store", void 0);
|
|
13005
13279
|
_defineProperty(this, "client", null);
|
|
13006
13280
|
_defineProperty(this, "sessionId", null);
|
|
13281
|
+
_defineProperty(this, "conversationId", null);
|
|
13007
13282
|
_defineProperty(this, "messages", []);
|
|
13008
13283
|
_defineProperty(this, "status", "idle");
|
|
13009
13284
|
_defineProperty(this, "seq", 0);
|
|
@@ -13027,6 +13302,26 @@ var SmoothAgentChat = (function(exports) {
|
|
|
13027
13302
|
get connectionStatus() {
|
|
13028
13303
|
return this.status;
|
|
13029
13304
|
}
|
|
13305
|
+
/** Conversation id of the live session, or null before connect (voice passes this as `conversation_id`). */
|
|
13306
|
+
get currentConversationId() {
|
|
13307
|
+
return this.conversationId;
|
|
13308
|
+
}
|
|
13309
|
+
/**
|
|
13310
|
+
* Append an already-finalized message to the transcript and emit — the voice
|
|
13311
|
+
* path reuses this so `transcript_final` (user) and `reply_text` (assistant)
|
|
13312
|
+
* turns land in the same message list / render pipeline as typed chat.
|
|
13313
|
+
*/
|
|
13314
|
+
appendLocalMessage(role, text) {
|
|
13315
|
+
const trimmed = text.trim();
|
|
13316
|
+
if (!trimmed) return;
|
|
13317
|
+
this.messages.push({
|
|
13318
|
+
id: this.nextId(role === "user" ? "u" : "a"),
|
|
13319
|
+
role,
|
|
13320
|
+
text: trimmed,
|
|
13321
|
+
streaming: false
|
|
13322
|
+
});
|
|
13323
|
+
this.emitMessages();
|
|
13324
|
+
}
|
|
13030
13325
|
/** The persisted store, exposed so the view can read identity for the pre-chat gate. */
|
|
13031
13326
|
getStore() {
|
|
13032
13327
|
return this.store;
|
|
@@ -13290,6 +13585,7 @@ var SmoothAgentChat = (function(exports) {
|
|
|
13290
13585
|
...metadata ? { metadata } : {}
|
|
13291
13586
|
});
|
|
13292
13587
|
this.sessionId = session.sessionId;
|
|
13588
|
+
this.conversationId = session.conversationId ?? null;
|
|
13293
13589
|
this.store.getState().setSessionId(session.sessionId);
|
|
13294
13590
|
}
|
|
13295
13591
|
/**
|
|
@@ -13306,6 +13602,7 @@ var SmoothAgentChat = (function(exports) {
|
|
|
13306
13602
|
}
|
|
13307
13603
|
if (snap.status === "ended") return false;
|
|
13308
13604
|
this.sessionId = sessionId;
|
|
13605
|
+
this.conversationId = snap.conversationId ?? null;
|
|
13309
13606
|
await this.hydrateHistory(sessionId);
|
|
13310
13607
|
return true;
|
|
13311
13608
|
}
|
|
@@ -13649,6 +13946,7 @@ var SmoothAgentChat = (function(exports) {
|
|
|
13649
13946
|
this.client?.disconnect("widget closed");
|
|
13650
13947
|
this.client = null;
|
|
13651
13948
|
this.sessionId = null;
|
|
13949
|
+
this.conversationId = null;
|
|
13652
13950
|
this.activeRequestId = null;
|
|
13653
13951
|
this.resumeAttempted = false;
|
|
13654
13952
|
this.setInterrupt(null);
|
|
@@ -14199,6 +14497,47 @@ ${mode === "fullpage" ? `
|
|
|
14199
14497
|
.send:hover { transform: translateY(-1px) scale(1.05); }
|
|
14200
14498
|
.send:active { transform: scale(.94); }
|
|
14201
14499
|
.send:disabled { opacity: .4; cursor: default; transform: none; box-shadow: none; }
|
|
14500
|
+
|
|
14501
|
+
/* Voice mic toggle (SMOODEV-2534) — ghost twin of .send; lights up while live. */
|
|
14502
|
+
.mic {
|
|
14503
|
+
width: 38px; height: 38px;
|
|
14504
|
+
flex: none;
|
|
14505
|
+
border: 1px solid color-mix(in srgb, var(--sac-border) 80%, transparent);
|
|
14506
|
+
border-radius: 13px;
|
|
14507
|
+
cursor: pointer;
|
|
14508
|
+
display: flex;
|
|
14509
|
+
align-items: center;
|
|
14510
|
+
justify-content: center;
|
|
14511
|
+
background: transparent;
|
|
14512
|
+
color: color-mix(in srgb, var(--sac-text) 65%, transparent);
|
|
14513
|
+
transition: transform .2s var(--sac-ease), color .2s ease, background .25s ease, box-shadow .25s ease;
|
|
14514
|
+
}
|
|
14515
|
+
.mic svg { width: 18px; height: 18px; }
|
|
14516
|
+
.mic:hover { color: var(--sac-text); transform: translateY(-1px) scale(1.05); }
|
|
14517
|
+
.mic:active { transform: scale(.94); }
|
|
14518
|
+
.mic.active {
|
|
14519
|
+
border-color: transparent;
|
|
14520
|
+
background: linear-gradient(150deg, var(--sac-primary), var(--sac-primary-2));
|
|
14521
|
+
color: var(--sac-primary-text);
|
|
14522
|
+
animation: sac-mic-pulse 1.6s ease-out infinite;
|
|
14523
|
+
}
|
|
14524
|
+
/* Listening → soft expanding ring off the button (mirrors the presence pulse). */
|
|
14525
|
+
@keyframes sac-mic-pulse {
|
|
14526
|
+
0% { box-shadow: 0 0 0 0 color-mix(in srgb, var(--sac-primary) 45%, transparent); }
|
|
14527
|
+
70% { box-shadow: 0 0 0 9px color-mix(in srgb, var(--sac-primary) 0%, transparent); }
|
|
14528
|
+
100% { box-shadow: 0 0 0 0 color-mix(in srgb, var(--sac-primary) 0%, transparent); }
|
|
14529
|
+
}
|
|
14530
|
+
/* Agent TTS playing → faster secondary-accent shimmer so "speaking" reads distinctly. */
|
|
14531
|
+
.mic.active.speaking {
|
|
14532
|
+
animation: sac-mic-speaking 0.9s ease-in-out infinite;
|
|
14533
|
+
}
|
|
14534
|
+
@keyframes sac-mic-speaking {
|
|
14535
|
+
0%, 100% { box-shadow: 0 0 0 2px color-mix(in srgb, var(--sac-secondary) 35%, transparent); }
|
|
14536
|
+
50% { box-shadow: 0 0 0 6px color-mix(in srgb, var(--sac-secondary) 12%, transparent); }
|
|
14537
|
+
}
|
|
14538
|
+
@media (prefers-reduced-motion: reduce) {
|
|
14539
|
+
.mic.active, .mic.active.speaking { animation: none; box-shadow: 0 0 0 2px color-mix(in srgb, var(--sac-primary) 40%, transparent); }
|
|
14540
|
+
}
|
|
14202
14541
|
.footer {
|
|
14203
14542
|
text-align: center;
|
|
14204
14543
|
margin-top: 9px;
|
|
@@ -14511,7 +14850,9 @@ ${mode === "fullpage" ? `
|
|
|
14511
14850
|
/** Tool-confirmation interrupt — a shield. */
|
|
14512
14851
|
shield: `<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M12 3 5 6v5c0 4.4 3 7.2 7 8.5 4-1.3 7-4.1 7-8.5V6l-7-3Z" stroke="currentColor" stroke-width="1.7" stroke-linejoin="round"/><path d="m9 11.5 2 2 4-4" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"/></svg>`,
|
|
14513
14852
|
/** Tool-activity chip — a wrench. */
|
|
14514
|
-
tool: `<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M14.7 6.3a3.5 3.5 0 0 0-4.6 4.3l-5 5a1.6 1.6 0 0 0 2.3 2.3l5-5a3.5 3.5 0 0 0 4.3-4.6l-2 2-1.7-.3-.3-1.7 2-2Z" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round"/></svg
|
|
14853
|
+
tool: `<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M14.7 6.3a3.5 3.5 0 0 0-4.6 4.3l-5 5a1.6 1.6 0 0 0 2.3 2.3l5-5a3.5 3.5 0 0 0 4.3-4.6l-2 2-1.7-.3-.3-1.7 2-2Z" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round"/></svg>`,
|
|
14854
|
+
/** Voice toggle — a microphone. */
|
|
14855
|
+
mic: `<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><rect x="9" y="3.5" width="6" height="11" rx="3" stroke="currentColor" stroke-width="1.7"/><path d="M5.5 11.5a6.5 6.5 0 0 0 13 0M12 18v2.5M9 20.5h6" stroke="currentColor" stroke-width="1.7" stroke-linecap="round"/></svg>`
|
|
14515
14856
|
};
|
|
14516
14857
|
/**
|
|
14517
14858
|
* The identity_intake card: the fields the agent requested (pre-chat form field
|
|
@@ -14664,6 +15005,11 @@ ${mode === "fullpage" ? `
|
|
|
14664
15005
|
_defineProperty(this, "identityRestore", { phase: "idle" });
|
|
14665
15006
|
_defineProperty(this, "allowChatRestore", true);
|
|
14666
15007
|
_defineProperty(this, "gating", false);
|
|
15008
|
+
_defineProperty(this, "voiceCfg", {
|
|
15009
|
+
enabled: false,
|
|
15010
|
+
url: ""
|
|
15011
|
+
});
|
|
15012
|
+
_defineProperty(this, "voiceSession", null);
|
|
14667
15013
|
_defineProperty(this, "panelEl", null);
|
|
14668
15014
|
_defineProperty(this, "launcherEl", null);
|
|
14669
15015
|
_defineProperty(this, "messagesEl", null);
|
|
@@ -14671,6 +15017,7 @@ ${mode === "fullpage" ? `
|
|
|
14671
15017
|
_defineProperty(this, "dotEl", null);
|
|
14672
15018
|
_defineProperty(this, "inputEl", null);
|
|
14673
15019
|
_defineProperty(this, "sendBtn", null);
|
|
15020
|
+
_defineProperty(this, "micBtn", null);
|
|
14674
15021
|
_defineProperty(this, "suggestionsEl", null);
|
|
14675
15022
|
_defineProperty(this, "streamBubbleEl", null);
|
|
14676
15023
|
_defineProperty(this, "streamMsgId", null);
|
|
@@ -14687,6 +15034,7 @@ ${mode === "fullpage" ? `
|
|
|
14687
15034
|
disconnectedCallback() {
|
|
14688
15035
|
this.mounted = false;
|
|
14689
15036
|
this.resetReveal();
|
|
15037
|
+
this.stopVoice();
|
|
14690
15038
|
this.controller?.disconnect();
|
|
14691
15039
|
this.controller = null;
|
|
14692
15040
|
}
|
|
@@ -14750,6 +15098,7 @@ ${mode === "fullpage" ? `
|
|
|
14750
15098
|
allowChatRestore: this.overrides.allowChatRestore,
|
|
14751
15099
|
allowAnonymous: this.overrides.allowAnonymous,
|
|
14752
15100
|
showToolActivity: this.overrides.showToolActivity ?? this.hasAttribute("show-tool-activity"),
|
|
15101
|
+
voice: this.overrides.voice,
|
|
14753
15102
|
theme
|
|
14754
15103
|
};
|
|
14755
15104
|
}
|
|
@@ -14834,6 +15183,8 @@ ${mode === "fullpage" ? `
|
|
|
14834
15183
|
</div>`;
|
|
14835
15184
|
const footerInner = [resolved.hideBranding ? "" : `<a href="${SMOOTH_OPERATOR_URL}" target="_blank" rel="noopener noreferrer">powered by <b>smooth‑operator</b></a>`, this.allowChatRestore ? `<button type="button" class="restore-link">Restore my chats</button>` : ""].filter(Boolean).join(" · ");
|
|
14836
15185
|
const footerHtml = footerInner ? `<div class="footer">${footerInner}</div>` : "";
|
|
15186
|
+
this.voiceCfg = resolved.voice;
|
|
15187
|
+
const micHtml = resolved.voice.enabled ? `<button class="mic" type="button" aria-label="Start voice" aria-pressed="false" title="Talk to ${escapeHtml(resolved.agentName)}">${ICON.mic}</button>` : "";
|
|
14837
15188
|
const chatHtml = `
|
|
14838
15189
|
<div class="messages"></div>
|
|
14839
15190
|
<div class="reply-suggestions"></div>
|
|
@@ -14841,6 +15192,7 @@ ${mode === "fullpage" ? `
|
|
|
14841
15192
|
<div class="composer-wrap">
|
|
14842
15193
|
<div class="composer">
|
|
14843
15194
|
<textarea rows="1" placeholder="${escapeHtml(resolved.placeholder)}"></textarea>
|
|
15195
|
+
${micHtml}
|
|
14844
15196
|
<button class="send" type="button" aria-label="Send message">${ICON.send}</button>
|
|
14845
15197
|
</div>
|
|
14846
15198
|
${footerHtml}
|
|
@@ -14858,6 +15210,7 @@ ${mode === "fullpage" ? `
|
|
|
14858
15210
|
const logoSvg = container.querySelector(".logo-wrap svg");
|
|
14859
15211
|
if (logoSvg) logoSvg.setAttribute("class", "logo");
|
|
14860
15212
|
this.resetReveal();
|
|
15213
|
+
this.stopVoice();
|
|
14861
15214
|
this.root.replaceChildren(style, container);
|
|
14862
15215
|
this.launcherEl = container.querySelector(".launcher");
|
|
14863
15216
|
this.panelEl = container.querySelector(".panel");
|
|
@@ -14866,11 +15219,13 @@ ${mode === "fullpage" ? `
|
|
|
14866
15219
|
this.dotEl = container.querySelector(".dot");
|
|
14867
15220
|
this.inputEl = container.querySelector("textarea");
|
|
14868
15221
|
this.sendBtn = container.querySelector(".send");
|
|
15222
|
+
this.micBtn = container.querySelector(".mic");
|
|
14869
15223
|
this.interruptEl = container.querySelector(".interrupt");
|
|
14870
15224
|
this.suggestionsEl = container.querySelector(".reply-suggestions");
|
|
14871
15225
|
this.launcherEl?.addEventListener("click", () => this.openChat());
|
|
14872
15226
|
container.querySelector(".close")?.addEventListener("click", () => this.closeChat());
|
|
14873
15227
|
this.sendBtn?.addEventListener("click", () => this.submit());
|
|
15228
|
+
this.micBtn?.addEventListener("click", () => this.toggleVoice());
|
|
14874
15229
|
this.inputEl?.addEventListener("input", () => this.autosize());
|
|
14875
15230
|
this.inputEl?.addEventListener("keydown", (ev) => {
|
|
14876
15231
|
if (ev.key === "Enter" && !ev.shiftKey) {
|
|
@@ -15645,6 +16000,84 @@ ${mode === "fullpage" ? `
|
|
|
15645
16000
|
if (this.sendBtn) this.sendBtn.disabled = busy;
|
|
15646
16001
|
if (this.inputEl) this.inputEl.disabled = busy;
|
|
15647
16002
|
}
|
|
16003
|
+
/**
|
|
16004
|
+
* Mic button: start a voice session, or — when one is live — end it. Hitting
|
|
16005
|
+
* the button while the agent's TTS is playing barges in first (interrupt +
|
|
16006
|
+
* playback flush) so the audio dies instantly, then the session ends.
|
|
16007
|
+
*/
|
|
16008
|
+
toggleVoice() {
|
|
16009
|
+
if (this.voiceSession) {
|
|
16010
|
+
if (this.voiceSession.isSpeaking) this.voiceSession.interrupt();
|
|
16011
|
+
this.stopVoice();
|
|
16012
|
+
return;
|
|
16013
|
+
}
|
|
16014
|
+
this.startVoice();
|
|
16015
|
+
}
|
|
16016
|
+
async startVoice() {
|
|
16017
|
+
if (!this.controller || this.voiceSession || !this.voiceCfg.enabled) return;
|
|
16018
|
+
const config = this.readConfig();
|
|
16019
|
+
if (!config) return;
|
|
16020
|
+
const conversationId = this.controller.currentConversationId ?? void 0;
|
|
16021
|
+
const controller = this.controller;
|
|
16022
|
+
const session = new VoiceSession({
|
|
16023
|
+
url: this.voiceCfg.url,
|
|
16024
|
+
agentId: config.agentId,
|
|
16025
|
+
conversationId
|
|
16026
|
+
}, {
|
|
16027
|
+
onTranscriptPartial: (text) => {
|
|
16028
|
+
if (this.inputEl) {
|
|
16029
|
+
this.inputEl.value = text;
|
|
16030
|
+
this.autosize();
|
|
16031
|
+
}
|
|
16032
|
+
},
|
|
16033
|
+
onTranscriptFinal: (text) => {
|
|
16034
|
+
if (this.inputEl) {
|
|
16035
|
+
this.inputEl.value = "";
|
|
16036
|
+
this.autosize();
|
|
16037
|
+
}
|
|
16038
|
+
controller.appendLocalMessage("user", text);
|
|
16039
|
+
},
|
|
16040
|
+
onReplyText: (text) => {
|
|
16041
|
+
controller.appendLocalMessage("assistant", text);
|
|
16042
|
+
},
|
|
16043
|
+
onSpeaking: (speaking) => {
|
|
16044
|
+
this.micBtn?.classList.toggle("speaking", speaking);
|
|
16045
|
+
},
|
|
16046
|
+
onError: () => this.stopVoice(),
|
|
16047
|
+
onEnded: () => {
|
|
16048
|
+
this.voiceSession = null;
|
|
16049
|
+
this.syncVoiceUi(false);
|
|
16050
|
+
}
|
|
16051
|
+
});
|
|
16052
|
+
this.voiceSession = session;
|
|
16053
|
+
this.syncVoiceUi(true);
|
|
16054
|
+
try {
|
|
16055
|
+
await session.start();
|
|
16056
|
+
} catch {
|
|
16057
|
+
this.voiceSession = null;
|
|
16058
|
+
this.syncVoiceUi(false);
|
|
16059
|
+
}
|
|
16060
|
+
}
|
|
16061
|
+
/** End any live voice session and reset the composer UI. Idempotent. */
|
|
16062
|
+
stopVoice() {
|
|
16063
|
+
const session = this.voiceSession;
|
|
16064
|
+
this.voiceSession = null;
|
|
16065
|
+
session?.stop();
|
|
16066
|
+
this.syncVoiceUi(false);
|
|
16067
|
+
}
|
|
16068
|
+
/** Toggle the mic button's listening state + clear the partial transcript. */
|
|
16069
|
+
syncVoiceUi(active) {
|
|
16070
|
+
this.micBtn?.classList.toggle("active", active);
|
|
16071
|
+
this.micBtn?.setAttribute("aria-pressed", String(active));
|
|
16072
|
+
this.micBtn?.setAttribute("aria-label", active ? "Stop voice" : "Start voice");
|
|
16073
|
+
if (!active) {
|
|
16074
|
+
this.micBtn?.classList.remove("speaking");
|
|
16075
|
+
if (this.inputEl && this.inputEl.value) {
|
|
16076
|
+
this.inputEl.value = "";
|
|
16077
|
+
this.autosize();
|
|
16078
|
+
}
|
|
16079
|
+
}
|
|
16080
|
+
}
|
|
15648
16081
|
submit() {
|
|
15649
16082
|
if (!this.inputEl || !this.controller) return;
|
|
15650
16083
|
const text = this.inputEl.value;
|