@olares/lares-mobile 0.15.31 → 0.15.32
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/package.json +2 -2
- package/src/App.vue +44 -11
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@olares/lares-mobile",
|
|
3
|
-
"version": "0.15.
|
|
3
|
+
"version": "0.15.32",
|
|
4
4
|
"description": "Lares chat component for LarePass",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"repository": {
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
"vue": "^3.2.0"
|
|
29
29
|
},
|
|
30
30
|
"dependencies": {
|
|
31
|
-
"@olares/lares-core": "0.15.
|
|
31
|
+
"@olares/lares-core": "0.15.32",
|
|
32
32
|
"markdown-it": "^14.3.0"
|
|
33
33
|
}
|
|
34
34
|
}
|
package/src/App.vue
CHANGED
|
@@ -595,6 +595,8 @@ export default {
|
|
|
595
595
|
const view = this.voicePanel.getSnapshot(); this.voicePanelState = view;
|
|
596
596
|
this.voiceCall = view.snapshot?.connection === "closed" ? null : view.snapshot;
|
|
597
597
|
this.voicePhase = view.snapshot?.capture || "idle"; this.voiceError = view.error || view.snapshot?.error || "";
|
|
598
|
+
// The call no longer owns the answer: a cancelled take gives it back.
|
|
599
|
+
if (this.voiceSuspended && view.snapshot?.connection !== "open") this.resumeSuspendedReadout();
|
|
598
600
|
});
|
|
599
601
|
this.replySpeaker = createReplySpeaker();
|
|
600
602
|
this.reader = createSentencePlayer({
|
|
@@ -648,7 +650,7 @@ export default {
|
|
|
648
650
|
this.voicePageShowHandler = () => {
|
|
649
651
|
if (this.voiceReadoutPaused) { this.voiceReadoutPaused = false; this.reader.resume(); }
|
|
650
652
|
};
|
|
651
|
-
this.voiceOfflineHandler = () => { this.
|
|
653
|
+
this.voiceOfflineHandler = () => { this.stopReadout(); void this.voicePanel.pause().catch(() => {}); };
|
|
652
654
|
window.addEventListener("offline", this.voiceOfflineHandler);
|
|
653
655
|
document.addEventListener("visibilitychange", this.voiceVisibilityHandler);
|
|
654
656
|
window.addEventListener("pagehide", this.voicePageHideHandler);
|
|
@@ -663,7 +665,7 @@ export default {
|
|
|
663
665
|
this.restoreLog();
|
|
664
666
|
},
|
|
665
667
|
deactivated() {
|
|
666
|
-
this.
|
|
668
|
+
this.stopReadout();
|
|
667
669
|
void this.voicePanel?.pause().catch(() => {});
|
|
668
670
|
this.captureLog();
|
|
669
671
|
},
|
|
@@ -742,7 +744,7 @@ export default {
|
|
|
742
744
|
}
|
|
743
745
|
if (sessionChanged && snap.sessionId !== this.voiceAdoptingSession) {
|
|
744
746
|
void this.voicePanel?.pause().catch(() => {});
|
|
745
|
-
this.
|
|
747
|
+
this.stopReadout();
|
|
746
748
|
this.player?.stop();
|
|
747
749
|
}
|
|
748
750
|
if (sessionChanged) this.voiceAdoptingSession = null;
|
|
@@ -759,9 +761,12 @@ export default {
|
|
|
759
761
|
// A paused call is not speaking. Only an open call owns the reply;
|
|
760
762
|
// leaving it set after the keyboard returns is how later answers stay silent.
|
|
761
763
|
const voiceSpeaking = this.voiceCall?.connection === "open";
|
|
762
|
-
if (voiceSpeaking) this.
|
|
764
|
+
if (voiceSpeaking) this.suspendReadout();
|
|
763
765
|
const target = this.voiceSpeak && !voiceSpeaking && !document.hidden ? speakable(utterance, this.voiceStreaming) : null;
|
|
764
|
-
if (target) this.
|
|
766
|
+
if (target) this.followReadout({ ...target, sessionId: snap.sessionId, source: { sessionId: snap.sessionId, turn: target.turn, step: target.step, attemptId: target.attemptId }, key: `${snap.sessionId}:${target.key}` });
|
|
767
|
+
// A cancelled voice take did not own the answer: play it again. A take
|
|
768
|
+
// that submitted a turn grew the transcript, so the call owns it.
|
|
769
|
+
if (!voiceSpeaking) this.resumeSuspendedReadout();
|
|
765
770
|
this.failed = snap.failed;
|
|
766
771
|
this.error = snap.error;
|
|
767
772
|
this.sessions = snap.sessions ?? this.sessions;
|
|
@@ -1302,7 +1307,7 @@ export default {
|
|
|
1302
1307
|
},
|
|
1303
1308
|
async onVoiceShortcut(event) {
|
|
1304
1309
|
if (this.chatDevice !== "desktop" || document.hidden || navigator.onLine === false || event.composedPath?.().some(node => node?.hasAttribute?.("data-voice-shortcut-recorder"))) return;
|
|
1305
|
-
if (event.key === "Escape" && !event.defaultPrevented) { this.
|
|
1310
|
+
if (event.key === "Escape" && !event.defaultPrevented) { this.stopReadout(); void this.voicePanel.pause().catch(() => {}); return; }
|
|
1306
1311
|
const action = matchVoiceShortcut(event, this.voiceSettings?.config?.dictationShortcuts);
|
|
1307
1312
|
if (!action || !laresVoiceAvailability(this.voiceSettings).dictation) return;
|
|
1308
1313
|
event.preventDefault();
|
|
@@ -1352,7 +1357,7 @@ export default {
|
|
|
1352
1357
|
const device = settings.config.device === "headset" ? "headset" : "device";
|
|
1353
1358
|
onDevice(device);
|
|
1354
1359
|
if (settings.config.backend === "realtime" && continuous) throw new Error("voice_manual_realtime_unavailable");
|
|
1355
|
-
this.
|
|
1360
|
+
this.suspendReadout(); this.player?.stop(); this.player?.unlock();
|
|
1356
1361
|
if (config.transport !== "legacy") {
|
|
1357
1362
|
return createUnifiedConversation({ request: (path, options) => this.runtime.voiceRequest(path, options), socket: (ticket) => this.runtime.voiceSocket(ticket),
|
|
1358
1363
|
device: captureSource("mobile", config.device), readAloud: continuous && config.speak, preset: config.preset, presentationSession: true,
|
|
@@ -1394,6 +1399,34 @@ export default {
|
|
|
1394
1399
|
onError: (error) => { this.voiceError = error.message; },
|
|
1395
1400
|
});
|
|
1396
1401
|
},
|
|
1402
|
+
followReadout(value) {
|
|
1403
|
+
if (!value) return;
|
|
1404
|
+
this.lastReadout = value;
|
|
1405
|
+
this.reader.follow(value);
|
|
1406
|
+
},
|
|
1407
|
+
suspendReadout() {
|
|
1408
|
+
const key = this.reader?.key;
|
|
1409
|
+
if (!key) return;
|
|
1410
|
+
if (this.lastReadout?.key === key) {
|
|
1411
|
+
this.voiceSuspended = this.lastReadout;
|
|
1412
|
+
this.voiceSuspendedItems = this.items?.length ?? 0;
|
|
1413
|
+
}
|
|
1414
|
+
this.reader.suspend?.();
|
|
1415
|
+
},
|
|
1416
|
+
resumeSuspendedReadout() {
|
|
1417
|
+
const suspended = this.voiceSuspended;
|
|
1418
|
+
if (!suspended) return;
|
|
1419
|
+
this.voiceSuspended = null;
|
|
1420
|
+
const taken = (this.items?.length ?? 0) > this.voiceSuspendedItems;
|
|
1421
|
+
if (taken) this.reader.consume(suspended.key);
|
|
1422
|
+
else if (suspended.sessionId === this.sessionId && this.voiceSpeak && !document.hidden) this.followReadout(suspended);
|
|
1423
|
+
},
|
|
1424
|
+
stopReadout() {
|
|
1425
|
+
this.voiceSuspended = null;
|
|
1426
|
+
this.voiceSuspendedItems = 0;
|
|
1427
|
+
this.lastReadout = null;
|
|
1428
|
+
this.reader.stop();
|
|
1429
|
+
},
|
|
1397
1430
|
setReadoutAwake(active) {
|
|
1398
1431
|
this.readoutActive = Boolean(active);
|
|
1399
1432
|
if (this.readoutActive) void this.readoutWake?.hold();
|
|
@@ -1415,7 +1448,7 @@ export default {
|
|
|
1415
1448
|
this.voiceSpeak = next;
|
|
1416
1449
|
this.speakBusy = true;
|
|
1417
1450
|
if (!next) {
|
|
1418
|
-
this.
|
|
1451
|
+
this.stopReadout();
|
|
1419
1452
|
this.player?.stop();
|
|
1420
1453
|
void this.runtime.interruptSpeech?.();
|
|
1421
1454
|
}
|
|
@@ -1580,10 +1613,10 @@ export default {
|
|
|
1580
1613
|
const text = this.draft.trim();
|
|
1581
1614
|
if (!this.canSend) return;
|
|
1582
1615
|
this.player?.unlock();
|
|
1583
|
-
// A typed reply is the chat's.
|
|
1584
|
-
//
|
|
1616
|
+
// A typed reply is the chat's. Stop the old readout before pausing the
|
|
1617
|
+
// call, so the pause cannot hand a suspended answer back for a moment.
|
|
1618
|
+
this.stopReadout();
|
|
1585
1619
|
if (this.voiceCall?.connection === "open") await this.voicePanel.pause();
|
|
1586
|
-
this.reader.stop();
|
|
1587
1620
|
const sessionId = this.sessionId;
|
|
1588
1621
|
const images = this.draftImages.filter((row) => draftImageStatus(row) === "ready");
|
|
1589
1622
|
const wasRunning = this.running;
|