@smooai/chat-widget 0.12.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/README.md +1 -0
- package/dist/chat-widget.global.js +676 -50
- package/dist/chat-widget.global.js.map +1 -1
- package/dist/index.d.ts +268 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +703 -51
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/config.ts +29 -1
- package/src/conversation.ts +116 -2
- package/src/element.ts +252 -15
- package/src/index.ts +21 -1
- package/src/styles.ts +83 -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.
|
|
@@ -11782,6 +12090,11 @@ var SmoothAgentChat = (function(exports) {
|
|
|
11782
12090
|
collectConsent: config.collectConsent ?? true,
|
|
11783
12091
|
allowChatRestore: config.allowChatRestore ?? true,
|
|
11784
12092
|
allowAnonymous: config.allowAnonymous ?? false,
|
|
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
|
+
},
|
|
11785
12098
|
theme: {
|
|
11786
12099
|
text: theme.text ?? "#f8fafc",
|
|
11787
12100
|
background: theme.background ?? "#040d30",
|
|
@@ -11804,44 +12117,6 @@ var SmoothAgentChat = (function(exports) {
|
|
|
11804
12117
|
return !resolved.allowAnonymous && (resolved.requireName || resolved.requireEmail || resolved.requirePhone);
|
|
11805
12118
|
}
|
|
11806
12119
|
//#endregion
|
|
11807
|
-
//#region \0@oxc-project+runtime@0.134.0/helpers/esm/typeof.js
|
|
11808
|
-
function _typeof(o) {
|
|
11809
|
-
"@babel/helpers - typeof";
|
|
11810
|
-
return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function(o) {
|
|
11811
|
-
return typeof o;
|
|
11812
|
-
} : function(o) {
|
|
11813
|
-
return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o;
|
|
11814
|
-
}, _typeof(o);
|
|
11815
|
-
}
|
|
11816
|
-
//#endregion
|
|
11817
|
-
//#region \0@oxc-project+runtime@0.134.0/helpers/esm/toPrimitive.js
|
|
11818
|
-
function toPrimitive(t, r) {
|
|
11819
|
-
if ("object" != _typeof(t) || !t) return t;
|
|
11820
|
-
var e = t[Symbol.toPrimitive];
|
|
11821
|
-
if (void 0 !== e) {
|
|
11822
|
-
var i = e.call(t, r || "default");
|
|
11823
|
-
if ("object" != _typeof(i)) return i;
|
|
11824
|
-
throw new TypeError("@@toPrimitive must return a primitive value.");
|
|
11825
|
-
}
|
|
11826
|
-
return ("string" === r ? String : Number)(t);
|
|
11827
|
-
}
|
|
11828
|
-
//#endregion
|
|
11829
|
-
//#region \0@oxc-project+runtime@0.134.0/helpers/esm/toPropertyKey.js
|
|
11830
|
-
function toPropertyKey(t) {
|
|
11831
|
-
var i = toPrimitive(t, "string");
|
|
11832
|
-
return "symbol" == _typeof(i) ? i : i + "";
|
|
11833
|
-
}
|
|
11834
|
-
//#endregion
|
|
11835
|
-
//#region \0@oxc-project+runtime@0.134.0/helpers/esm/defineProperty.js
|
|
11836
|
-
function _defineProperty(e, r, t) {
|
|
11837
|
-
return (r = toPropertyKey(r)) in e ? Object.defineProperty(e, r, {
|
|
11838
|
-
value: t,
|
|
11839
|
-
enumerable: !0,
|
|
11840
|
-
configurable: !0,
|
|
11841
|
-
writable: !0
|
|
11842
|
-
}) : e[r] = t, e;
|
|
11843
|
-
}
|
|
11844
|
-
//#endregion
|
|
11845
12120
|
//#region node_modules/.pnpm/@smooai+smooth-operator@1.21.1/node_modules/@smooai/smooth-operator/dist/transport.js
|
|
11846
12121
|
/**
|
|
11847
12122
|
* Transport abstraction for the client.
|
|
@@ -12943,6 +13218,59 @@ var SmoothAgentChat = (function(exports) {
|
|
|
12943
13218
|
streaming: false
|
|
12944
13219
|
};
|
|
12945
13220
|
}
|
|
13221
|
+
let toolSeq = 0;
|
|
13222
|
+
const nextToolId = () => `tool-${++toolSeq}`;
|
|
13223
|
+
/** Grow the trailing text block, or open a new one if the last block was a tool. */
|
|
13224
|
+
function growTextBlock(blocks, text) {
|
|
13225
|
+
if (!text) return;
|
|
13226
|
+
const last = blocks[blocks.length - 1];
|
|
13227
|
+
if (last && last.kind === "text") last.text += text;
|
|
13228
|
+
else blocks.push({
|
|
13229
|
+
kind: "text",
|
|
13230
|
+
text
|
|
13231
|
+
});
|
|
13232
|
+
}
|
|
13233
|
+
/**
|
|
13234
|
+
* Fold a `stream_chunk` node-state into the ordered block list, returning `true`
|
|
13235
|
+
* when the chunk carried tool activity.
|
|
13236
|
+
*
|
|
13237
|
+
* Tool activity rides `state.rawResponse.toolCall` / `state.rawResponse.toolResult`
|
|
13238
|
+
* — **NOT** `state.toolResult`. Reading the wrong path leaves every chip stuck on
|
|
13239
|
+
* "running…" forever (the exact bug the daemon SPA hit and this mirror avoids).
|
|
13240
|
+
*/
|
|
13241
|
+
function applyToolChunk(blocks, state) {
|
|
13242
|
+
const raw = state?.rawResponse;
|
|
13243
|
+
if (!raw || typeof raw !== "object") return false;
|
|
13244
|
+
const call = raw.toolCall;
|
|
13245
|
+
const res = raw.toolResult;
|
|
13246
|
+
if (call) {
|
|
13247
|
+
const args = typeof call.arguments === "string" ? call.arguments : JSON.stringify(call.arguments ?? {});
|
|
13248
|
+
blocks.push({
|
|
13249
|
+
kind: "tool",
|
|
13250
|
+
tool: {
|
|
13251
|
+
id: nextToolId(),
|
|
13252
|
+
name: call.name ?? "",
|
|
13253
|
+
args,
|
|
13254
|
+
done: false
|
|
13255
|
+
}
|
|
13256
|
+
});
|
|
13257
|
+
return true;
|
|
13258
|
+
}
|
|
13259
|
+
if (res) {
|
|
13260
|
+
const result = typeof res.result === "string" ? res.result : JSON.stringify(res.result ?? "");
|
|
13261
|
+
for (let i = blocks.length - 1; i >= 0; i--) {
|
|
13262
|
+
const b = blocks[i];
|
|
13263
|
+
if (b && b.kind === "tool" && b.tool.name === (res.name ?? "") && !b.tool.done) {
|
|
13264
|
+
b.tool.done = true;
|
|
13265
|
+
b.tool.isError = !!res.isError;
|
|
13266
|
+
b.tool.result = result;
|
|
13267
|
+
break;
|
|
13268
|
+
}
|
|
13269
|
+
}
|
|
13270
|
+
return true;
|
|
13271
|
+
}
|
|
13272
|
+
return false;
|
|
13273
|
+
}
|
|
12946
13274
|
var ConversationController = class {
|
|
12947
13275
|
constructor(config, events, store) {
|
|
12948
13276
|
_defineProperty(this, "config", void 0);
|
|
@@ -12950,6 +13278,7 @@ var SmoothAgentChat = (function(exports) {
|
|
|
12950
13278
|
_defineProperty(this, "store", void 0);
|
|
12951
13279
|
_defineProperty(this, "client", null);
|
|
12952
13280
|
_defineProperty(this, "sessionId", null);
|
|
13281
|
+
_defineProperty(this, "conversationId", null);
|
|
12953
13282
|
_defineProperty(this, "messages", []);
|
|
12954
13283
|
_defineProperty(this, "status", "idle");
|
|
12955
13284
|
_defineProperty(this, "seq", 0);
|
|
@@ -12973,6 +13302,26 @@ var SmoothAgentChat = (function(exports) {
|
|
|
12973
13302
|
get connectionStatus() {
|
|
12974
13303
|
return this.status;
|
|
12975
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
|
+
}
|
|
12976
13325
|
/** The persisted store, exposed so the view can read identity for the pre-chat gate. */
|
|
12977
13326
|
getStore() {
|
|
12978
13327
|
return this.store;
|
|
@@ -13236,6 +13585,7 @@ var SmoothAgentChat = (function(exports) {
|
|
|
13236
13585
|
...metadata ? { metadata } : {}
|
|
13237
13586
|
});
|
|
13238
13587
|
this.sessionId = session.sessionId;
|
|
13588
|
+
this.conversationId = session.conversationId ?? null;
|
|
13239
13589
|
this.store.getState().setSessionId(session.sessionId);
|
|
13240
13590
|
}
|
|
13241
13591
|
/**
|
|
@@ -13252,6 +13602,7 @@ var SmoothAgentChat = (function(exports) {
|
|
|
13252
13602
|
}
|
|
13253
13603
|
if (snap.status === "ended") return false;
|
|
13254
13604
|
this.sessionId = sessionId;
|
|
13605
|
+
this.conversationId = snap.conversationId ?? null;
|
|
13255
13606
|
await this.hydrateHistory(sessionId);
|
|
13256
13607
|
return true;
|
|
13257
13608
|
}
|
|
@@ -13289,11 +13640,13 @@ var SmoothAgentChat = (function(exports) {
|
|
|
13289
13640
|
text: trimmed,
|
|
13290
13641
|
streaming: false
|
|
13291
13642
|
});
|
|
13643
|
+
const showTools = this.config.showToolActivity === true;
|
|
13292
13644
|
const assistant = {
|
|
13293
13645
|
id: this.nextId("a"),
|
|
13294
13646
|
role: "assistant",
|
|
13295
13647
|
text: "",
|
|
13296
|
-
streaming: true
|
|
13648
|
+
streaming: true,
|
|
13649
|
+
blocks: showTools ? [] : void 0
|
|
13297
13650
|
};
|
|
13298
13651
|
this.messages.push(assistant);
|
|
13299
13652
|
this.emitMessages();
|
|
@@ -13308,8 +13661,11 @@ var SmoothAgentChat = (function(exports) {
|
|
|
13308
13661
|
const token = event.token ?? event.data?.token ?? "";
|
|
13309
13662
|
if (token) {
|
|
13310
13663
|
assistant.text += token;
|
|
13664
|
+
if (showTools && assistant.blocks) growTextBlock(assistant.blocks, token);
|
|
13311
13665
|
this.emitMessages();
|
|
13312
13666
|
}
|
|
13667
|
+
} else if (showTools && event.type === "stream_chunk") {
|
|
13668
|
+
if (assistant.blocks && applyToolChunk(assistant.blocks, event.data?.state)) this.emitMessages();
|
|
13313
13669
|
} else this.handleTurnEvent(event);
|
|
13314
13670
|
const inner = (await turn).data?.data;
|
|
13315
13671
|
const finalText = extractFinalText(inner?.response);
|
|
@@ -13319,6 +13675,7 @@ var SmoothAgentChat = (function(exports) {
|
|
|
13319
13675
|
if (citations.length > 0) assistant.citations = citations;
|
|
13320
13676
|
const suggestions = extractSuggestions(inner?.response);
|
|
13321
13677
|
if (suggestions.length > 0) assistant.suggestions = suggestions;
|
|
13678
|
+
if (assistant.blocks && !assistant.blocks.some((b) => b.kind === "tool")) assistant.blocks = void 0;
|
|
13322
13679
|
assistant.streaming = false;
|
|
13323
13680
|
this.emitMessages();
|
|
13324
13681
|
} catch (err) {
|
|
@@ -13589,6 +13946,7 @@ var SmoothAgentChat = (function(exports) {
|
|
|
13589
13946
|
this.client?.disconnect("widget closed");
|
|
13590
13947
|
this.client = null;
|
|
13591
13948
|
this.sessionId = null;
|
|
13949
|
+
this.conversationId = null;
|
|
13592
13950
|
this.activeRequestId = null;
|
|
13593
13951
|
this.resumeAttempted = false;
|
|
13594
13952
|
this.setInterrupt(null);
|
|
@@ -13630,6 +13988,7 @@ var SmoothAgentChat = (function(exports) {
|
|
|
13630
13988
|
--sac-bg: ${theme.background};
|
|
13631
13989
|
--sac-primary: ${theme.primary};
|
|
13632
13990
|
--sac-primary-text: ${theme.primaryText};
|
|
13991
|
+
--sac-secondary: ${theme.secondary};
|
|
13633
13992
|
--sac-assistant-bubble: ${theme.assistantBubble};
|
|
13634
13993
|
--sac-assistant-bubble-text: ${theme.assistantBubbleText};
|
|
13635
13994
|
--sac-user-bubble: ${theme.userBubble};
|
|
@@ -13942,6 +14301,47 @@ ${mode === "fullpage" ? `
|
|
|
13942
14301
|
}
|
|
13943
14302
|
@keyframes sac-blink { to { opacity: 0 } }
|
|
13944
14303
|
|
|
14304
|
+
/* Interleaved tool-activity strip (gated by showToolActivity): prose bubbles
|
|
14305
|
+
and inline tool chips stacked in the order the model produced them. */
|
|
14306
|
+
.blocks { display: flex; flex-direction: column; align-items: flex-start; gap: 7px; }
|
|
14307
|
+
.blocks .bubble { align-self: flex-start; }
|
|
14308
|
+
.toolchip {
|
|
14309
|
+
display: inline-flex;
|
|
14310
|
+
align-items: center;
|
|
14311
|
+
gap: 6px;
|
|
14312
|
+
max-width: 100%;
|
|
14313
|
+
padding: 5px 10px;
|
|
14314
|
+
border-radius: 99px;
|
|
14315
|
+
font-size: 12px;
|
|
14316
|
+
line-height: 1.3;
|
|
14317
|
+
color: color-mix(in srgb, var(--sac-text) 78%, transparent);
|
|
14318
|
+
background: color-mix(in srgb, var(--sac-text) 6%, transparent);
|
|
14319
|
+
border: 1px solid color-mix(in srgb, var(--sac-text) 12%, transparent);
|
|
14320
|
+
animation: sac-msg-in .3s var(--sac-ease) both;
|
|
14321
|
+
}
|
|
14322
|
+
.toolchip .ti { display: inline-flex; flex: none; opacity: .8; }
|
|
14323
|
+
.toolchip .ti svg { width: 13px; height: 13px; }
|
|
14324
|
+
.toolchip .tn { font-weight: 600; letter-spacing: .01em; }
|
|
14325
|
+
.toolchip .ts { opacity: .7; }
|
|
14326
|
+
.toolchip .ta {
|
|
14327
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
14328
|
+
font-size: 11px;
|
|
14329
|
+
opacity: .6;
|
|
14330
|
+
overflow: hidden;
|
|
14331
|
+
text-overflow: ellipsis;
|
|
14332
|
+
white-space: nowrap;
|
|
14333
|
+
min-width: 0;
|
|
14334
|
+
}
|
|
14335
|
+
.toolchip.running { border-color: color-mix(in srgb, var(--sac-primary) 45%, transparent); }
|
|
14336
|
+
.toolchip.running .ts { color: var(--sac-primary); opacity: 1; animation: sac-typing 1.3s ease-in-out infinite; }
|
|
14337
|
+
.toolchip.done .ts::before { content: '✓ '; }
|
|
14338
|
+
.toolchip.error {
|
|
14339
|
+
color: var(--sac-secondary);
|
|
14340
|
+
border-color: color-mix(in srgb, var(--sac-secondary) 50%, transparent);
|
|
14341
|
+
background: color-mix(in srgb, var(--sac-secondary) 10%, transparent);
|
|
14342
|
+
}
|
|
14343
|
+
.toolchip.error .ts::before { content: '! '; }
|
|
14344
|
+
|
|
13945
14345
|
/* ─────────────── Rendered markdown (assistant bubbles / snippets) ─────────── */
|
|
13946
14346
|
/* The renderer (markdown.ts) emits a small allowlisted set of tags; these rules
|
|
13947
14347
|
keep them legible inside the tight Aurora-Glass bubble + citation card. */
|
|
@@ -14097,6 +14497,47 @@ ${mode === "fullpage" ? `
|
|
|
14097
14497
|
.send:hover { transform: translateY(-1px) scale(1.05); }
|
|
14098
14498
|
.send:active { transform: scale(.94); }
|
|
14099
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
|
+
}
|
|
14100
14541
|
.footer {
|
|
14101
14542
|
text-align: center;
|
|
14102
14543
|
margin-top: 9px;
|
|
@@ -14384,7 +14825,8 @@ ${mode === "fullpage" ? `
|
|
|
14384
14825
|
"greeting",
|
|
14385
14826
|
"start-open",
|
|
14386
14827
|
"mode",
|
|
14387
|
-
"hide-branding"
|
|
14828
|
+
"hide-branding",
|
|
14829
|
+
"show-tool-activity"
|
|
14388
14830
|
];
|
|
14389
14831
|
/**
|
|
14390
14832
|
* Inline SVG icons (static, trusted strings — never interpolated with user data).
|
|
@@ -14406,7 +14848,11 @@ ${mode === "fullpage" ? `
|
|
|
14406
14848
|
/** Identity-intake interrupt — a person. */
|
|
14407
14849
|
user: `<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><circle cx="12" cy="8.2" r="3.4" stroke="currentColor" stroke-width="1.7"/><path d="M5.5 19.5c.8-3.1 3.4-4.8 6.5-4.8s5.7 1.7 6.5 4.8" stroke="currentColor" stroke-width="1.7" stroke-linecap="round"/></svg>`,
|
|
14408
14850
|
/** Tool-confirmation interrupt — a shield. */
|
|
14409
|
-
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
|
|
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>`,
|
|
14852
|
+
/** Tool-activity chip — a wrench. */
|
|
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>`
|
|
14410
14856
|
};
|
|
14411
14857
|
/**
|
|
14412
14858
|
* The identity_intake card: the fields the agent requested (pre-chat form field
|
|
@@ -14559,6 +15005,11 @@ ${mode === "fullpage" ? `
|
|
|
14559
15005
|
_defineProperty(this, "identityRestore", { phase: "idle" });
|
|
14560
15006
|
_defineProperty(this, "allowChatRestore", true);
|
|
14561
15007
|
_defineProperty(this, "gating", false);
|
|
15008
|
+
_defineProperty(this, "voiceCfg", {
|
|
15009
|
+
enabled: false,
|
|
15010
|
+
url: ""
|
|
15011
|
+
});
|
|
15012
|
+
_defineProperty(this, "voiceSession", null);
|
|
14562
15013
|
_defineProperty(this, "panelEl", null);
|
|
14563
15014
|
_defineProperty(this, "launcherEl", null);
|
|
14564
15015
|
_defineProperty(this, "messagesEl", null);
|
|
@@ -14566,12 +15017,14 @@ ${mode === "fullpage" ? `
|
|
|
14566
15017
|
_defineProperty(this, "dotEl", null);
|
|
14567
15018
|
_defineProperty(this, "inputEl", null);
|
|
14568
15019
|
_defineProperty(this, "sendBtn", null);
|
|
15020
|
+
_defineProperty(this, "micBtn", null);
|
|
14569
15021
|
_defineProperty(this, "suggestionsEl", null);
|
|
14570
15022
|
_defineProperty(this, "streamBubbleEl", null);
|
|
14571
15023
|
_defineProperty(this, "streamMsgId", null);
|
|
14572
15024
|
_defineProperty(this, "streamTarget", "");
|
|
14573
15025
|
_defineProperty(this, "displayedLength", 0);
|
|
14574
15026
|
_defineProperty(this, "rafId", 0);
|
|
15027
|
+
_defineProperty(this, "prevBlockSig", "");
|
|
14575
15028
|
this.root = this.attachShadow({ mode: "open" });
|
|
14576
15029
|
}
|
|
14577
15030
|
connectedCallback() {
|
|
@@ -14581,6 +15034,7 @@ ${mode === "fullpage" ? `
|
|
|
14581
15034
|
disconnectedCallback() {
|
|
14582
15035
|
this.mounted = false;
|
|
14583
15036
|
this.resetReveal();
|
|
15037
|
+
this.stopVoice();
|
|
14584
15038
|
this.controller?.disconnect();
|
|
14585
15039
|
this.controller = null;
|
|
14586
15040
|
}
|
|
@@ -14643,6 +15097,8 @@ ${mode === "fullpage" ? `
|
|
|
14643
15097
|
collectConsent: this.overrides.collectConsent,
|
|
14644
15098
|
allowChatRestore: this.overrides.allowChatRestore,
|
|
14645
15099
|
allowAnonymous: this.overrides.allowAnonymous,
|
|
15100
|
+
showToolActivity: this.overrides.showToolActivity ?? this.hasAttribute("show-tool-activity"),
|
|
15101
|
+
voice: this.overrides.voice,
|
|
14646
15102
|
theme
|
|
14647
15103
|
};
|
|
14648
15104
|
}
|
|
@@ -14727,6 +15183,8 @@ ${mode === "fullpage" ? `
|
|
|
14727
15183
|
</div>`;
|
|
14728
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(" · ");
|
|
14729
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>` : "";
|
|
14730
15188
|
const chatHtml = `
|
|
14731
15189
|
<div class="messages"></div>
|
|
14732
15190
|
<div class="reply-suggestions"></div>
|
|
@@ -14734,6 +15192,7 @@ ${mode === "fullpage" ? `
|
|
|
14734
15192
|
<div class="composer-wrap">
|
|
14735
15193
|
<div class="composer">
|
|
14736
15194
|
<textarea rows="1" placeholder="${escapeHtml(resolved.placeholder)}"></textarea>
|
|
15195
|
+
${micHtml}
|
|
14737
15196
|
<button class="send" type="button" aria-label="Send message">${ICON.send}</button>
|
|
14738
15197
|
</div>
|
|
14739
15198
|
${footerHtml}
|
|
@@ -14751,6 +15210,7 @@ ${mode === "fullpage" ? `
|
|
|
14751
15210
|
const logoSvg = container.querySelector(".logo-wrap svg");
|
|
14752
15211
|
if (logoSvg) logoSvg.setAttribute("class", "logo");
|
|
14753
15212
|
this.resetReveal();
|
|
15213
|
+
this.stopVoice();
|
|
14754
15214
|
this.root.replaceChildren(style, container);
|
|
14755
15215
|
this.launcherEl = container.querySelector(".launcher");
|
|
14756
15216
|
this.panelEl = container.querySelector(".panel");
|
|
@@ -14759,11 +15219,13 @@ ${mode === "fullpage" ? `
|
|
|
14759
15219
|
this.dotEl = container.querySelector(".dot");
|
|
14760
15220
|
this.inputEl = container.querySelector("textarea");
|
|
14761
15221
|
this.sendBtn = container.querySelector(".send");
|
|
15222
|
+
this.micBtn = container.querySelector(".mic");
|
|
14762
15223
|
this.interruptEl = container.querySelector(".interrupt");
|
|
14763
15224
|
this.suggestionsEl = container.querySelector(".reply-suggestions");
|
|
14764
15225
|
this.launcherEl?.addEventListener("click", () => this.openChat());
|
|
14765
15226
|
container.querySelector(".close")?.addEventListener("click", () => this.closeChat());
|
|
14766
15227
|
this.sendBtn?.addEventListener("click", () => this.submit());
|
|
15228
|
+
this.micBtn?.addEventListener("click", () => this.toggleVoice());
|
|
14767
15229
|
this.inputEl?.addEventListener("input", () => this.autosize());
|
|
14768
15230
|
this.inputEl?.addEventListener("keydown", (ev) => {
|
|
14769
15231
|
if (ev.key === "Enter" && !ev.shiftKey) {
|
|
@@ -15182,15 +15644,38 @@ ${mode === "fullpage" ? `
|
|
|
15182
15644
|
const prev = this.messages;
|
|
15183
15645
|
const last = messages[messages.length - 1];
|
|
15184
15646
|
const prevLast = prev[prev.length - 1];
|
|
15185
|
-
const structural = messages.length !== prev.length || !last || !prevLast || last.id !== prevLast.id || last.role !== prevLast.role || last.streaming !== prevLast.streaming || !!last.streaming && !prevLast.text && !!last.text;
|
|
15647
|
+
const structural = messages.length !== prev.length || !last || !prevLast || last.id !== prevLast.id || last.role !== prevLast.role || last.streaming !== prevLast.streaming || !!last.streaming && !prevLast.text && !!last.text || this.blockSig(last) !== this.prevBlockSig;
|
|
15186
15648
|
this.messages = messages;
|
|
15187
|
-
|
|
15188
|
-
|
|
15649
|
+
this.prevBlockSig = this.blockSig(last);
|
|
15650
|
+
if (!structural && last && last.streaming && this.tailKey(last) === this.streamMsgId) {
|
|
15651
|
+
this.streamTarget = this.tailText(last);
|
|
15189
15652
|
this.ensureRevealLoop();
|
|
15190
15653
|
return;
|
|
15191
15654
|
}
|
|
15192
15655
|
this.renderMessages();
|
|
15193
15656
|
}
|
|
15657
|
+
/** True when an assistant message's turn invoked at least one tool. */
|
|
15658
|
+
hasToolBlocks(m) {
|
|
15659
|
+
return !!m?.blocks?.some((b) => b.kind === "tool");
|
|
15660
|
+
}
|
|
15661
|
+
/** Signature that changes only when a tool chip is added or resolved (text growth is 'x'). */
|
|
15662
|
+
blockSig(m) {
|
|
15663
|
+
if (!m?.blocks) return "";
|
|
15664
|
+
return m.blocks.map((b) => b.kind === "tool" ? `t:${b.tool.id}:${b.tool.done ? 1 : 0}` : "x").join("|");
|
|
15665
|
+
}
|
|
15666
|
+
/** The live (last) text block for a tool turn, else the whole message text. */
|
|
15667
|
+
tailText(m) {
|
|
15668
|
+
if (this.hasToolBlocks(m) && m.blocks) {
|
|
15669
|
+
const last = m.blocks[m.blocks.length - 1];
|
|
15670
|
+
return last?.kind === "text" ? last.text : "";
|
|
15671
|
+
}
|
|
15672
|
+
return m.text;
|
|
15673
|
+
}
|
|
15674
|
+
/** Reveal-binding key — composite for a tool-turn tail (so a new trailing block rebinds), else msg id. */
|
|
15675
|
+
tailKey(m) {
|
|
15676
|
+
if (this.hasToolBlocks(m) && m.blocks) return `${m.id}#${m.blocks.length - 1}`;
|
|
15677
|
+
return m.id;
|
|
15678
|
+
}
|
|
15194
15679
|
renderMessages() {
|
|
15195
15680
|
if (!this.messagesEl) return;
|
|
15196
15681
|
this.resetReveal();
|
|
@@ -15211,6 +15696,11 @@ ${mode === "fullpage" ? `
|
|
|
15211
15696
|
this.messagesEl.appendChild(chips);
|
|
15212
15697
|
}
|
|
15213
15698
|
for (const msg of this.messages) {
|
|
15699
|
+
if (msg.role === "assistant" && this.hasToolBlocks(msg)) {
|
|
15700
|
+
this.messagesEl.appendChild(this.buildRow("assistant", this.renderAssistantBlocks(msg)));
|
|
15701
|
+
if (!msg.streaming && msg.citations && msg.citations.length > 0) this.messagesEl.appendChild(this.renderSources(msg.citations));
|
|
15702
|
+
continue;
|
|
15703
|
+
}
|
|
15214
15704
|
const bubble = document.createElement("div");
|
|
15215
15705
|
bubble.className = `bubble ${msg.role}`;
|
|
15216
15706
|
if (msg.role === "assistant" && msg.streaming && !msg.text) {
|
|
@@ -15268,19 +15758,77 @@ ${mode === "fullpage" ? `
|
|
|
15268
15758
|
* doesn't restart the reveal from zero), then resumes the loop.
|
|
15269
15759
|
*/
|
|
15270
15760
|
bindReveal(msg, bubble) {
|
|
15271
|
-
const
|
|
15761
|
+
const key = this.tailKey(msg);
|
|
15762
|
+
const target = this.tailText(msg);
|
|
15763
|
+
const carryOver = key === this.streamMsgId ? Math.min(this.displayedLength, target.length) : 0;
|
|
15272
15764
|
this.streamBubbleEl = bubble;
|
|
15273
|
-
this.streamMsgId =
|
|
15274
|
-
this.streamTarget =
|
|
15765
|
+
this.streamMsgId = key;
|
|
15766
|
+
this.streamTarget = target;
|
|
15275
15767
|
this.displayedLength = carryOver;
|
|
15276
15768
|
if (this.prefersReducedMotion()) {
|
|
15277
|
-
this.displayedLength =
|
|
15278
|
-
bubble.textContent =
|
|
15769
|
+
this.displayedLength = target.length;
|
|
15770
|
+
bubble.textContent = target;
|
|
15279
15771
|
return;
|
|
15280
15772
|
}
|
|
15281
|
-
bubble.textContent =
|
|
15773
|
+
bubble.textContent = target.slice(0, this.displayedLength);
|
|
15282
15774
|
this.ensureRevealLoop();
|
|
15283
15775
|
}
|
|
15776
|
+
/**
|
|
15777
|
+
* Render a tool-activity assistant turn as an ordered strip: prose bubbles and
|
|
15778
|
+
* inline tool chips in the order the model produced them (mirrors the daemon
|
|
15779
|
+
* SPA's `blocks`). The live trailing text block (while streaming) binds to the
|
|
15780
|
+
* rAF reveal; earlier/finalized text blocks render as sanitized markdown.
|
|
15781
|
+
*/
|
|
15782
|
+
renderAssistantBlocks(msg) {
|
|
15783
|
+
const wrap = document.createElement("div");
|
|
15784
|
+
wrap.className = "blocks";
|
|
15785
|
+
const blocks = msg.blocks ?? [];
|
|
15786
|
+
const lastIdx = blocks.length - 1;
|
|
15787
|
+
blocks.forEach((block, i) => {
|
|
15788
|
+
if (block.kind === "tool") {
|
|
15789
|
+
wrap.appendChild(this.buildToolChip(block.tool));
|
|
15790
|
+
return;
|
|
15791
|
+
}
|
|
15792
|
+
const bubble = document.createElement("div");
|
|
15793
|
+
bubble.className = "bubble assistant";
|
|
15794
|
+
if (msg.streaming && i === lastIdx) {
|
|
15795
|
+
bubble.classList.add("cursor");
|
|
15796
|
+
this.bindReveal(msg, bubble);
|
|
15797
|
+
} else {
|
|
15798
|
+
bubble.classList.add("md");
|
|
15799
|
+
bubble.innerHTML = renderMarkdown(block.text);
|
|
15800
|
+
}
|
|
15801
|
+
wrap.appendChild(bubble);
|
|
15802
|
+
});
|
|
15803
|
+
return wrap;
|
|
15804
|
+
}
|
|
15805
|
+
/**
|
|
15806
|
+
* A single tool-activity chip: icon + tool name + status (running… / done / error),
|
|
15807
|
+
* with a truncated args preview. Tool name/args are set via `textContent` so a
|
|
15808
|
+
* tool payload can never inject markup.
|
|
15809
|
+
*/
|
|
15810
|
+
buildToolChip(tool) {
|
|
15811
|
+
const chip = document.createElement("div");
|
|
15812
|
+
chip.className = `toolchip ${tool.done ? tool.isError ? "error" : "done" : "running"}`;
|
|
15813
|
+
chip.setAttribute("part", "tool-chip");
|
|
15814
|
+
const icon = document.createElement("span");
|
|
15815
|
+
icon.className = "ti";
|
|
15816
|
+
icon.innerHTML = ICON.tool;
|
|
15817
|
+
const name = document.createElement("span");
|
|
15818
|
+
name.className = "tn";
|
|
15819
|
+
name.textContent = tool.name || "tool";
|
|
15820
|
+
const status = document.createElement("span");
|
|
15821
|
+
status.className = "ts";
|
|
15822
|
+
status.textContent = tool.done ? tool.isError ? "error" : "done" : "running…";
|
|
15823
|
+
chip.append(icon, name, status);
|
|
15824
|
+
if (tool.args && tool.args !== "{}" && tool.args !== "\"\"") {
|
|
15825
|
+
const args = document.createElement("span");
|
|
15826
|
+
args.className = "ta";
|
|
15827
|
+
args.textContent = tool.args.length > 80 ? `${tool.args.slice(0, 80)}…` : tool.args;
|
|
15828
|
+
chip.appendChild(args);
|
|
15829
|
+
}
|
|
15830
|
+
return chip;
|
|
15831
|
+
}
|
|
15284
15832
|
/** Start the rAF loop if it isn't already running. */
|
|
15285
15833
|
ensureRevealLoop() {
|
|
15286
15834
|
if (this.prefersReducedMotion() || typeof requestAnimationFrame !== "function") {
|
|
@@ -15452,6 +16000,84 @@ ${mode === "fullpage" ? `
|
|
|
15452
16000
|
if (this.sendBtn) this.sendBtn.disabled = busy;
|
|
15453
16001
|
if (this.inputEl) this.inputEl.disabled = busy;
|
|
15454
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
|
+
}
|
|
15455
16081
|
submit() {
|
|
15456
16082
|
if (!this.inputEl || !this.controller) return;
|
|
15457
16083
|
const text = this.inputEl.value;
|