@olares/lares-mobile 0.15.30 → 0.15.31

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@olares/lares-mobile",
3
- "version": "0.15.30",
3
+ "version": "0.15.31",
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.30",
31
+ "@olares/lares-core": "0.15.31",
32
32
  "markdown-it": "^14.3.0"
33
33
  }
34
34
  }
package/src/App.vue CHANGED
@@ -127,6 +127,7 @@ import { createVoiceConversation } from "@olares/lares-core/voice/conversation";
127
127
  import { createUnifiedConversation } from "@olares/lares-core/voice/unified-conversation";
128
128
  import { createAnswerPlayer } from "./chat/playback.js";
129
129
  import { createReplySpeaker, createSentencePlayer, readoutState, speakable } from "@olares/lares-core/voice/speech";
130
+ import { createScreenWakeLock } from "@olares/lares-core/voice/wake-lock";
130
131
  import { prepareBrowserCapture } from "@olares/lares-core/voice/browser-capture";
131
132
  import { playAudioStream, createStreamingCapture } from "@olares/lares-core/voice/stream-client";
132
133
  import { voiceHeaders, wavBlob } from "@olares/lares-core/voice/client";
@@ -561,6 +562,9 @@ export default {
561
562
  this.intake = new FileIntake((file, sessionId, options) => this.runtime.upload(file, options, sessionId));
562
563
  this.imageUploadControllers = new Map();
563
564
  this.player = createAnswerPlayer();
565
+ // A locked screen suspends the WebView and cuts a long answer short; hold
566
+ // the screen for as long as a readout is actually playing.
567
+ this.readoutWake = createScreenWakeLock();
564
568
  this.voicePanel = createVoicePanel({
565
569
  armOutput: () => this.player?.unlock(),
566
570
  create: (options) => this.createMobileVoiceConversation(options),
@@ -607,6 +611,7 @@ export default {
607
611
  halt: () => this.player?.stop(),
608
612
  silence: (source) => void this.runtime.interruptSpeech?.(source),
609
613
  fail: (err) => this.showSpeakProblem(err),
614
+ onState: (active) => this.setReadoutAwake(active),
610
615
  });
611
616
 
612
617
  this.bindIntake(this.sessionId);
@@ -624,12 +629,30 @@ export default {
624
629
  this.conversationUnsub = subscribeConversationSettings((section) => {
625
630
  this.busyEnter = section.busyEnter;
626
631
  });
632
+ this.voiceReadoutPaused = false;
627
633
  this.voiceVisibilityHandler = () => {
628
- if (document.hidden) { this.reader.stop(); void this.voicePanel.pause().catch(() => {}); }
634
+ if (document.hidden) {
635
+ // A locked screen or an app switch is a pause, not the end of the
636
+ // answer: stopping here marks the utterance read and it never resumes.
637
+ if (this.reader.canPause && this.reader.pause()) this.voiceReadoutPaused = true;
638
+ void this.voicePanel.pause().catch(() => {});
639
+ return;
640
+ }
641
+ if (this.readoutActive) void this.readoutWake?.hold();
642
+ if (this.voiceReadoutPaused) { this.voiceReadoutPaused = false; this.reader.resume(); }
643
+ };
644
+ this.voicePageHideHandler = () => {
645
+ if (this.reader.canPause && this.reader.pause()) this.voiceReadoutPaused = true;
646
+ void this.voicePanel.pause().catch(() => {});
647
+ };
648
+ this.voicePageShowHandler = () => {
649
+ if (this.voiceReadoutPaused) { this.voiceReadoutPaused = false; this.reader.resume(); }
629
650
  };
630
651
  this.voiceOfflineHandler = () => { this.reader.stop(); void this.voicePanel.pause().catch(() => {}); };
631
652
  window.addEventListener("offline", this.voiceOfflineHandler);
632
653
  document.addEventListener("visibilitychange", this.voiceVisibilityHandler);
654
+ window.addEventListener("pagehide", this.voicePageHideHandler);
655
+ window.addEventListener("pageshow", this.voicePageShowHandler);
633
656
  window.addEventListener("keydown", this.onVoiceShortcut, true);
634
657
  this.voiceClock = setInterval(() => {
635
658
  if (this.voicePhase === "recording") this.voiceElapsed += 1;
@@ -647,6 +670,9 @@ export default {
647
670
  beforeUnmount() {
648
671
  window.removeEventListener("offline", this.voiceOfflineHandler);
649
672
  document.removeEventListener("visibilitychange", this.voiceVisibilityHandler);
673
+ window.removeEventListener("pagehide", this.voicePageHideHandler);
674
+ window.removeEventListener("pageshow", this.voicePageShowHandler);
675
+ this.readoutWake?.release();
650
676
  window.removeEventListener("keydown", this.onVoiceShortcut, true);
651
677
  clearInterval(this.voiceClock);
652
678
  this.captureLog();
@@ -1340,7 +1366,7 @@ export default {
1340
1366
  send: (value) => conversation.speak(value),
1341
1367
  play: (sentence) => sentence.bluetooth === "played" ? Promise.resolve() : this.player.play(wavBlob(sentence.audio)),
1342
1368
  halt: () => this.player.stop(), silence: () => {},
1343
- onState: (playing) => conversation?.playbackState(playing),
1369
+ onState: (playing) => { conversation?.playbackState(playing); this.setReadoutAwake(playing); },
1344
1370
  onOutput: (value) => readoutState.publish(value?.status === "error" ? null : value),
1345
1371
  fail: (error) => { void conversation.controller.disconnect(error?.code || error?.message || "voice_playback_failed"); },
1346
1372
  });
@@ -1368,6 +1394,11 @@ export default {
1368
1394
  onError: (error) => { this.voiceError = error.message; },
1369
1395
  });
1370
1396
  },
1397
+ setReadoutAwake(active) {
1398
+ this.readoutActive = Boolean(active);
1399
+ if (this.readoutActive) void this.readoutWake?.hold();
1400
+ else this.readoutWake?.release();
1401
+ },
1371
1402
  showSpeakProblem(err) {
1372
1403
  this.speakNotice = speakProblemText(this.t, err);
1373
1404
  },
@@ -17,6 +17,10 @@
17
17
  <p v-if="view.errors.ttsModel" class="lares-tts__alert" role="alert">{{ t('settings.saveFailed') }}</p>
18
18
  <p v-if="!state.model" class="lares-tts__hint">{{ t('preferences.modelsMissing') }}</p>
19
19
  <p v-else-if="state.loading" class="lares-tts__hint" role="status">{{ t('tts.loading') }}</p>
20
+ <div v-if="state.stale" class="lares-tts__status" role="status">
21
+ <p>{{ t('tts.stale') }}</p>
22
+ <button type="button" @click="controller.load()">{{ t('dictation.retry') }}</button>
23
+ </div>
20
24
  <div v-else-if="state.error" class="lares-tts__status" role="alert">
21
25
  <p>{{ t('tts.loadFailed') }}</p>
22
26
  <button type="button" @click="controller.load()">{{ t('dictation.retry') }}</button>