@olares/lares-mobile 0.15.32 → 0.15.33

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.32",
3
+ "version": "0.15.33",
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.32",
31
+ "@olares/lares-core": "0.15.33",
32
32
  "markdown-it": "^14.3.0"
33
33
  }
34
34
  }
package/src/App.vue CHANGED
@@ -1327,20 +1327,27 @@ export default {
1327
1327
  const view = this.voicePanel.getSnapshot();
1328
1328
  if (view.busy) return;
1329
1329
  if (view.visible && view.mode === "single") { if (view.snapshot?.capture === "recording") await this.voicePanel.finish().catch(() => {}); return; }
1330
- await this.voicePanel.pause().catch(() => {});
1331
1330
  await this.voicePanel.open({ mode: "single", sessionId: this.sessionId || null, preset: preset && preset !== "default" ? preset : undefined, configSnapshot: this.voiceSettings.config }).catch(() => {});
1332
1331
  },
1333
- async onHoldStart() {
1332
+ onHoldStart() {
1334
1333
  this.voicePanel.arm();
1335
1334
  if (this.voicePanelState?.mode === "single" && this.voicePanelState.raw) {
1336
- await this.voicePanel.open({ mode: "single", sessionId: this.sessionId || null });
1337
- return;
1335
+ return this.voicePanel.open({ mode: "single", sessionId: this.sessionId || null });
1338
1336
  }
1337
+ const settings = this.voiceSettings;
1338
+ if (!settings) return this.openVoiceFromSettings();
1339
+ this.voiceDefaultMode = settings.config.mobileMode;
1340
+ const available = laresVoiceAvailability(settings, !this.modelBlocked);
1341
+ if (!(this.voiceDefaultMode === "single" ? available.dictation : available.conversation)) { this.voiceError = "voice_model_unavailable"; return; }
1342
+ return this.voicePanel.open({ mode: this.voiceDefaultMode, sessionId: this.sessionId || null, configSnapshot: settings.config });
1343
+ },
1344
+ async openVoiceFromSettings() {
1339
1345
  const settings = await this.runtime.settings.voice(false, { diagnostics: false });
1346
+ this.voiceSettings = markRaw(settings);
1340
1347
  this.voiceDefaultMode = settings.config.mobileMode;
1341
1348
  const available = laresVoiceAvailability(settings, !this.modelBlocked);
1342
1349
  if (!(this.voiceDefaultMode === "single" ? available.dictation : available.conversation)) { this.voiceError = "voice_model_unavailable"; return; }
1343
- await this.voicePanel.open({ mode: this.voiceDefaultMode, sessionId: this.sessionId || null });
1350
+ return this.voicePanel.open({ mode: this.voiceDefaultMode, sessionId: this.sessionId || null, configSnapshot: settings.config });
1344
1351
  },
1345
1352
  onVoiceAction({ action, value }) {
1346
1353
  if (action === "start" || action === "end" || action === "close") this.speakNotice = "";
@@ -1350,7 +1357,7 @@ export default {
1350
1357
  if (["holdStart", "holdEnd", "finish", "start", "close", "end"].includes(action)) void Promise.resolve(this.voicePanel[action](value)).catch(() => {});
1351
1358
  },
1352
1359
  runVoicePanel(action) { void this.voicePanel[action]().catch(() => {}); },
1353
- async createMobileVoiceConversation({ mode, onTranscript, onDevice, config }) {
1360
+ async createMobileVoiceConversation({ mode, onTranscript, onDevice, config, preparation }) {
1354
1361
  config = laresVoiceConfig(config, mode);
1355
1362
  const settings = { config };
1356
1363
  const continuous = mode === "continuous";
@@ -1362,7 +1369,7 @@ export default {
1362
1369
  return createUnifiedConversation({ request: (path, options) => this.runtime.voiceRequest(path, options), socket: (ticket) => this.runtime.voiceSocket(ticket),
1363
1370
  device: captureSource("mobile", config.device), readAloud: continuous && config.speak, preset: config.preset, presentationSession: true,
1364
1371
  endpointSilenceMs: config.endpointSilenceMs, idleTimeoutMs: config.idleTimeoutMs, recoveryTimeoutMs: config.recoveryTimeoutMs,
1365
- onTranscript: continuous ? null : onTranscript, mode: () => resolveSubmitMode(this.running, false, true, this.busyEnter),
1372
+ onTranscript: continuous ? null : onTranscript, mode: () => resolveSubmitMode(this.running, false, true, this.busyEnter), preparation,
1366
1373
  transcribe: (blob, signal) => this.runtime.transcribe(blob, config.language, signal, config.model) });
1367
1374
  }
1368
1375
  let conversation;
@@ -1384,7 +1391,7 @@ export default {
1384
1391
  readAloud: continuous && settings.config.speak,
1385
1392
  preset: settings.config.preset, backend: settings.config.backend, endpointSilenceMs: settings.config.endpointSilenceMs, idleTimeoutMs: settings.config.idleTimeoutMs, recoveryTimeoutMs: settings.config.recoveryTimeoutMs,
1386
1393
  presentationSession: true,
1387
- onTranscript: continuous ? null : onTranscript,
1394
+ onTranscript: continuous ? null : onTranscript, preparation,
1388
1395
  mode: () => resolveSubmitMode(this.running, false, true, this.busyEnter),
1389
1396
  playback: { get canPause() { return reader.canPause; }, get key() { return reader.key; }, pause: () => reader.pause(), resume: () => reader.resume(), stop: () => { reader.stop(); this.player.stop(); } },
1390
1397
  onReply: (reply) => { const value = speakable(reply, config.streaming === true); if (value) reader.follow(value); },
@@ -9,6 +9,9 @@
9
9
  <p>{{ t("settings.readAloudSyncFailed") }}</p>
10
10
  <button type="button" @click="$emit('voice-action', { action: 'retryReadAloud' })">{{ t("dictation.retry") }}</button>
11
11
  </div>
12
+ <div v-else-if="voiceError && !voiceView?.inputActive" class="lares-composer__alert" role="alert">
13
+ <p>{{ voiceError }}</p>
14
+ </div>
12
15
  <div
13
16
  class="lares-composer__card"
14
17
  data-composer-card
@@ -93,10 +96,6 @@
93
96
  </button>
94
97
  </figure>
95
98
  </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>
100
99
  <div class="lares-composer__line" :data-expanded="expanded ? 'true' : 'false'">
101
100
  <button
102
101
  type="button"
@@ -174,7 +173,7 @@
174
173
 
175
174
  <script>
176
175
  import { messageFromCode } from "@olares/lares-core/i18n/t";
177
- import { voiceCallFailure, voicePlaybackChrome } from "@olares/lares-core/voice/presentation";
176
+ import { voiceCallFailure } from "@olares/lares-core/voice/presentation";
178
177
  import LaresVoiceInput from "./VoiceInput.vue";
179
178
  import {
180
179
  REFERENCE_DEBOUNCE_MS,
@@ -256,9 +255,6 @@ export default {
256
255
  callFailure() {
257
256
  return this.voiceView?.inputActive ? voiceCallFailure(this.voiceView) : null;
258
257
  },
259
- playbackChrome() {
260
- return voicePlaybackChrome(this.voiceView);
261
- },
262
258
  callFailureText() {
263
259
  return this.callFailure ? messageFromCode(this.t, this.callFailure.code, "error.voice_failed") : "";
264
260
  },
@@ -291,9 +287,6 @@ export default {
291
287
  window.removeEventListener("pointerdown", this.onTokenPointer, true);
292
288
  },
293
289
  methods: {
294
- stopPlayback() {
295
- this.$emit("voice-action", { action: "stopPlayback" });
296
- },
297
290
  onInput(event) {
298
291
  const value = event.target.value;
299
292
  this.$emit("update:draft", value);
@@ -713,34 +706,6 @@ export default {
713
706
  stroke-linecap: round;
714
707
  }
715
708
 
716
- .lares-composer__playback {
717
- display: flex;
718
- align-items: center;
719
- justify-content: space-between;
720
- gap: 12px;
721
- padding: 4px 4px 8px;
722
- color: var(--q-ink-2);
723
- font-size: 13px;
724
- line-height: 20px;
725
- }
726
-
727
- .lares-composer__playback button {
728
- flex: none;
729
- min-height: 32px;
730
- padding: 4px 12px;
731
- border: 1px solid var(--q-separator);
732
- border-radius: 999px;
733
- background: transparent;
734
- color: var(--q-ink-1);
735
- font: inherit;
736
- font-size: 13px;
737
- }
738
-
739
- .lares-composer__playback button:focus-visible {
740
- outline: 2px solid var(--q-blue-default, #3377ff);
741
- outline-offset: 2px;
742
- }
743
-
744
709
  .lares-composer__line {
745
710
  display: flex;
746
711
  width: 100%;
@@ -81,7 +81,10 @@
81
81
  <span>{{ speechLabel }}</span>
82
82
  <button type="button" class="lares-message-stop" @click="readout.stop()">{{ t(readout.status === 'error' ? 'readout.dismiss' : 'readout.stop') }}</button>
83
83
  </div>
84
- <button v-if="!row.pending && !readout && isSpeaking(row)" type="button" class="lares-message-stop" @click="$emit('stop-speaking')">{{ t('input.stopSpeaking') }}</button>
84
+ <div v-else-if="ownsPlayback(row)" role="status" class="lares-message-readout">
85
+ <span>{{ t(playbackChrome.status) }}</span>
86
+ <button type="button" class="lares-message-stop" @click="$emit('stop-speaking')">{{ t(playbackChrome.stopLabel) }}</button>
87
+ </div>
85
88
  <LaresMessageActions
86
89
  v-if="!row.pending"
87
90
  :text="row.text"
@@ -111,6 +114,7 @@
111
114
 
112
115
  <script>
113
116
  import { readoutState } from "@olares/lares-core/voice/speech";
117
+ import { voicePlaybackChrome } from "@olares/lares-core/voice/presentation";
114
118
  import { partitionPreviews } from "@olares/lares-core/files/preview-groups";
115
119
  import { parseAskUserQuestions } from "@olares/lares-core/larepass/questions";
116
120
  import { hidesRunningClock, processFoldLabel, transcriptBlocks } from "@olares/lares-core/larepass/process-fold";
@@ -159,6 +163,7 @@ export default {
159
163
  },
160
164
  computed: {
161
165
  speechLabel() { return this.t(`readout.${this.readout?.status}`); },
166
+ playbackChrome() { return voicePlaybackChrome(this.voiceView); },
162
167
  runningText() {
163
168
  return runningLabel(this.runningSince, this.t, this.now);
164
169
  },
@@ -197,11 +202,12 @@ export default {
197
202
  const latest = this.items.findLast(item => item.type === "assistant" && item.turn === this.readout.turn);
198
203
  return latest && (row.id || row.key) === (latest.id || latest.key);
199
204
  },
200
- isSpeaking(row) {
201
- const snapshot = this.voiceView?.snapshot;
202
- if (snapshot?.playback !== "playing") return false;
203
- const latest = this.items.findLast(item => item.type === "assistant" && (item.voiceId || item.voicePlayback?.voiceId) === snapshot.voiceId);
204
- return latest && (row.id || row.key) === (latest.id || latest.key);
205
+ ownsPlayback(row) {
206
+ if (!this.playbackChrome || row.type !== "assistant") return false;
207
+ const voiceId = this.voiceView?.snapshot?.voiceId;
208
+ const matched = voiceId && this.items.findLast((item) => item.type === "assistant" && (item.voiceId || item.voicePlayback?.voiceId) === voiceId);
209
+ const target = matched || this.items.findLast((item) => item.type === "assistant");
210
+ return Boolean(target) && (row.id || row.key) === (target.id || target.key);
205
211
  },
206
212
  syncClock() {
207
213
  clearInterval(this.clock);