@olares/lares-mobile 0.15.27 → 0.15.29

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.27",
3
+ "version": "0.15.29",
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.27",
31
+ "@olares/lares-core": "0.15.29",
32
32
  "markdown-it": "^14.3.0"
33
33
  }
34
34
  }
package/src/App.vue CHANGED
@@ -730,8 +730,11 @@ export default {
730
730
  this.items = snap.items;
731
731
  this.running = snap.running;
732
732
  this.runningSince = snap.runningSince || 0;
733
- if (this.voiceCall) this.reader.stop();
734
- const target = this.voiceSpeak && !this.voiceCall && !document.hidden ? speakable(utterance, this.voiceStreaming) : null;
733
+ // A paused call is not speaking. Only an open call owns the reply;
734
+ // leaving it set after the keyboard returns is how later answers stay silent.
735
+ const voiceSpeaking = this.voiceCall?.connection === "open";
736
+ if (voiceSpeaking) this.reader.stop();
737
+ const target = this.voiceSpeak && !voiceSpeaking && !document.hidden ? speakable(utterance, this.voiceStreaming) : null;
735
738
  if (target) this.reader.follow({ ...target, sessionId: snap.sessionId, source: { sessionId: snap.sessionId, turn: target.turn, step: target.step, attemptId: target.attemptId }, key: `${snap.sessionId}:${target.key}` });
736
739
  this.failed = snap.failed;
737
740
  this.error = snap.error;
@@ -1328,7 +1331,8 @@ export default {
1328
1331
  return createUnifiedConversation({ request: (path, options) => this.runtime.voiceRequest(path, options), socket: (ticket) => this.runtime.voiceSocket(ticket),
1329
1332
  device: captureSource("mobile", config.device), readAloud: continuous && config.speak, preset: config.preset, presentationSession: true,
1330
1333
  endpointSilenceMs: config.endpointSilenceMs, idleTimeoutMs: config.idleTimeoutMs, recoveryTimeoutMs: config.recoveryTimeoutMs,
1331
- onTranscript: continuous ? null : onTranscript, mode: () => resolveSubmitMode(this.running, false, true, this.busyEnter) });
1334
+ onTranscript: continuous ? null : onTranscript, mode: () => resolveSubmitMode(this.running, false, true, this.busyEnter),
1335
+ transcribe: (blob, signal) => this.runtime.transcribe(blob, config.language, signal, config.model) });
1332
1336
  }
1333
1337
  let conversation;
1334
1338
  const reader = createSentencePlayer({
@@ -1545,11 +1549,9 @@ export default {
1545
1549
  const text = this.draft.trim();
1546
1550
  if (!this.canSend) return;
1547
1551
  this.player?.unlock();
1548
- const continuous = this.voicePanelState?.mode === "continuous";
1549
- if (this.voiceSpeak && continuous) {
1550
- try { await this.voicePanel.openReadout(); }
1551
- catch (error) { this.voiceError = error?.code || error?.message || "voice_failed"; }
1552
- } else if (this.voiceCall?.connection === "open") await this.voicePanel.pause();
1552
+ // A typed reply is the chat's. An open voice call would keep the reader
1553
+ // quiet and then not speak this text, so pause it and let the reader take the answer.
1554
+ if (this.voiceCall?.connection === "open") await this.voicePanel.pause();
1553
1555
  this.reader.stop();
1554
1556
  const sessionId = this.sessionId;
1555
1557
  const images = this.draftImages.filter((row) => draftImageStatus(row) === "ready");
@@ -93,6 +93,10 @@
93
93
  </button>
94
94
  </figure>
95
95
  </div>
96
+ <div v-if="playbackChrome" class="lares-composer__playback" role="status">
97
+ <span>{{ t(playbackChrome.status) }}</span>
98
+ <button type="button" @click="stopPlayback">{{ t(playbackChrome.stopLabel) }}</button>
99
+ </div>
96
100
  <div class="lares-composer__line" :data-expanded="expanded ? 'true' : 'false'">
97
101
  <button
98
102
  type="button"
@@ -170,7 +174,8 @@
170
174
 
171
175
  <script>
172
176
  import { messageFromCode } from "@olares/lares-core/i18n/t";
173
- import { voiceCallFailure } from "@olares/lares-core/voice/presentation";
177
+ import { voiceCallFailure, voicePlaybackChrome } from "@olares/lares-core/voice/presentation";
178
+ import { readoutState } from "@olares/lares-core/voice/speech";
174
179
  import LaresVoiceInput from "./VoiceInput.vue";
175
180
  import {
176
181
  REFERENCE_DEBOUNCE_MS,
@@ -214,6 +219,8 @@ export default {
214
219
  ],
215
220
  data() {
216
221
  return {
222
+ readout: null,
223
+ readoutUnsub: null,
217
224
  inputMultiline: false,
218
225
  inputWidth: 0,
219
226
  inputResizeObserver: null,
@@ -252,6 +259,9 @@ export default {
252
259
  callFailure() {
253
260
  return this.voiceView?.inputActive ? voiceCallFailure(this.voiceView) : null;
254
261
  },
262
+ playbackChrome() {
263
+ return voicePlaybackChrome(this.voiceView, this.readout);
264
+ },
255
265
  callFailureText() {
256
266
  return this.callFailure ? messageFromCode(this.t, this.callFailure.code, "error.voice_failed") : "";
257
267
  },
@@ -269,6 +279,8 @@ export default {
269
279
  },
270
280
  },
271
281
  mounted() {
282
+ this.readout = readoutState.getSnapshot();
283
+ this.readoutUnsub = readoutState.subscribe(() => { this.readout = readoutState.getSnapshot(); });
272
284
  this.autosize();
273
285
  this.inputResizeObserver = new ResizeObserver(([entry]) => {
274
286
  const width = entry.contentRect.width;
@@ -279,11 +291,16 @@ export default {
279
291
  this.inputResizeObserver.observe(this.$refs.input);
280
292
  },
281
293
  beforeUnmount() {
294
+ this.readoutUnsub?.();
282
295
  this.inputResizeObserver?.disconnect();
283
296
  window.clearTimeout(this.referenceTimer);
284
297
  window.removeEventListener("pointerdown", this.onTokenPointer, true);
285
298
  },
286
299
  methods: {
300
+ stopPlayback() {
301
+ if (this.playbackChrome?.stop === "readout") this.readout?.stop?.();
302
+ else this.$emit("voice-action", { action: "stopPlayback" });
303
+ },
287
304
  onInput(event) {
288
305
  const value = event.target.value;
289
306
  this.$emit("update:draft", value);
@@ -703,6 +720,34 @@ export default {
703
720
  stroke-linecap: round;
704
721
  }
705
722
 
723
+ .lares-composer__playback {
724
+ display: flex;
725
+ align-items: center;
726
+ justify-content: space-between;
727
+ gap: 12px;
728
+ padding: 4px 4px 8px;
729
+ color: var(--q-ink-2);
730
+ font-size: 13px;
731
+ line-height: 20px;
732
+ }
733
+
734
+ .lares-composer__playback button {
735
+ flex: none;
736
+ min-height: 32px;
737
+ padding: 4px 12px;
738
+ border: 1px solid var(--q-separator);
739
+ border-radius: 999px;
740
+ background: transparent;
741
+ color: var(--q-ink-1);
742
+ font: inherit;
743
+ font-size: 13px;
744
+ }
745
+
746
+ .lares-composer__playback button:focus-visible {
747
+ outline: 2px solid var(--q-blue-default, #3377ff);
748
+ outline-offset: 2px;
749
+ }
750
+
706
751
  .lares-composer__line {
707
752
  display: flex;
708
753
  width: 100%;