@olares/lares-mobile 0.15.22 → 0.15.23
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 +12 -5
- package/src/chat/Composer.vue +21 -2
- package/src/chat/VoiceInput.vue +2 -3
- package/src/chat/playback.js +3 -0
- package/src/mobile/MobileShell.vue +28 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@olares/lares-mobile",
|
|
3
|
-
"version": "0.15.
|
|
3
|
+
"version": "0.15.23",
|
|
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.23",
|
|
32
32
|
"markdown-it": "^14.3.0"
|
|
33
33
|
}
|
|
34
34
|
}
|
package/src/App.vue
CHANGED
|
@@ -101,6 +101,7 @@
|
|
|
101
101
|
@open-references="loadReferences"
|
|
102
102
|
@pick-command="pickCommand"
|
|
103
103
|
@toggle-speak="toggleSpeak"
|
|
104
|
+
@dismiss-speak="speakNotice = ''"
|
|
104
105
|
/>
|
|
105
106
|
<LaresVoicePanel v-if="voicePanelState" :view="voicePanelState" :t="t"
|
|
106
107
|
@close="runVoicePanel('close')" @edit="voicePanel.edit($event)" @preset="voicePanel.selectPreset($event, true).catch(() => {})"
|
|
@@ -124,7 +125,7 @@ import { captureSource } from "@olares/lares-core/voice/input";
|
|
|
124
125
|
import { laresVoiceConfig, laresVoiceAvailability } from "@olares/lares-core/voice/lares-policy";
|
|
125
126
|
import { createVoiceConversation } from "@olares/lares-core/voice/conversation";
|
|
126
127
|
import { createAnswerPlayer } from "./chat/playback.js";
|
|
127
|
-
import { createReplySpeaker, createSentencePlayer, speakable } from "@olares/lares-core/voice/speech";
|
|
128
|
+
import { createReplySpeaker, createSentencePlayer, readoutState, speakable } from "@olares/lares-core/voice/speech";
|
|
128
129
|
import { prepareBrowserCapture } from "@olares/lares-core/voice/browser-capture";
|
|
129
130
|
import { playAudioStream, createStreamingCapture } from "@olares/lares-core/voice/stream-client";
|
|
130
131
|
import { voiceHeaders, wavBlob } from "@olares/lares-core/voice/client";
|
|
@@ -558,6 +559,7 @@ export default {
|
|
|
558
559
|
this.imageUploadControllers = new Map();
|
|
559
560
|
this.player = createAnswerPlayer();
|
|
560
561
|
this.voicePanel = createVoicePanel({
|
|
562
|
+
armOutput: () => this.player?.unlock(),
|
|
561
563
|
create: (options) => this.createMobileVoiceConversation(options),
|
|
562
564
|
prepareInput: (config) => config?.device === "headset" ? null : prepareBrowserCapture(),
|
|
563
565
|
loadConfig: async () => (await this.runtime.settings.voice(false, { diagnostics: false })).config,
|
|
@@ -1275,6 +1277,7 @@ export default {
|
|
|
1275
1277
|
await this.openDictation();
|
|
1276
1278
|
},
|
|
1277
1279
|
async openDictation(preset) {
|
|
1280
|
+
this.voicePanel.arm();
|
|
1278
1281
|
if (document.hidden || navigator.onLine === false) return;
|
|
1279
1282
|
if (!this.voiceSettings) {
|
|
1280
1283
|
try { this.voiceSettings = markRaw(await this.runtime.settings.voice(false, { diagnostics: false })); }
|
|
@@ -1288,6 +1291,7 @@ export default {
|
|
|
1288
1291
|
await this.voicePanel.open({ mode: "single", sessionId: this.sessionId || null, preset: preset && preset !== "default" ? preset : undefined, configSnapshot: this.voiceSettings.config }).catch(() => {});
|
|
1289
1292
|
},
|
|
1290
1293
|
async onHoldStart() {
|
|
1294
|
+
this.voicePanel.arm();
|
|
1291
1295
|
if (this.voicePanelState?.mode === "single" && this.voicePanelState.raw) {
|
|
1292
1296
|
await this.voicePanel.open({ mode: "single", sessionId: this.sessionId || null });
|
|
1293
1297
|
return;
|
|
@@ -1299,10 +1303,11 @@ export default {
|
|
|
1299
1303
|
await this.voicePanel.open({ mode: this.voiceDefaultMode, sessionId: this.sessionId || null });
|
|
1300
1304
|
},
|
|
1301
1305
|
onVoiceAction({ action, value }) {
|
|
1306
|
+
if (action === "start" || action === "end" || action === "close") this.speakNotice = "";
|
|
1302
1307
|
if (action === "retryReadAloud") { void this.voicePanel.retryReadAloud(); return; }
|
|
1303
1308
|
if (action === "stopPlayback") { void this.voicePanel.conversation?.controller.stopPlayback().catch(() => {}); return; }
|
|
1304
1309
|
if (action === "setCancelArmed") { this.voicePanel.setCancelArmed(value); return; }
|
|
1305
|
-
if (["holdStart", "holdEnd", "finish", "start", "close"].includes(action)) void Promise.resolve(this.voicePanel[action](value)).catch(() => {});
|
|
1310
|
+
if (["holdStart", "holdEnd", "finish", "start", "close", "end"].includes(action)) void Promise.resolve(this.voicePanel[action](value)).catch(() => {});
|
|
1306
1311
|
},
|
|
1307
1312
|
runVoicePanel(action) { void this.voicePanel[action]().catch(() => {}); },
|
|
1308
1313
|
async createMobileVoiceConversation({ mode, onTranscript, onDevice, config }) {
|
|
@@ -1312,7 +1317,7 @@ export default {
|
|
|
1312
1317
|
const device = settings.config.device === "headset" ? "headset" : "device";
|
|
1313
1318
|
onDevice(device);
|
|
1314
1319
|
if (settings.config.backend === "realtime" && continuous) throw new Error("voice_manual_realtime_unavailable");
|
|
1315
|
-
this.reader.stop(); this.player?.stop();
|
|
1320
|
+
this.reader.stop(); this.player?.stop();
|
|
1316
1321
|
let conversation;
|
|
1317
1322
|
const reader = createSentencePlayer({
|
|
1318
1323
|
stream: (value) => playAudioStream({ ...value, request: (path, options) => this.runtime.voiceRequest(path, options), headers: conversation.headers, socket: (ticket) => this.runtime.voiceSocket(ticket, "/api/lares/voice/stream") }),
|
|
@@ -1320,7 +1325,8 @@ export default {
|
|
|
1320
1325
|
play: (sentence) => sentence.bluetooth === "played" ? Promise.resolve() : this.player.play(wavBlob(sentence.audio)),
|
|
1321
1326
|
halt: () => this.player.stop(), silence: () => {},
|
|
1322
1327
|
onState: (playing) => conversation?.playbackState(playing),
|
|
1323
|
-
|
|
1328
|
+
onOutput: (value) => readoutState.publish(value?.status === "error" ? null : value),
|
|
1329
|
+
fail: (error) => { void conversation.controller.disconnect(error?.code || error?.message || "voice_playback_failed"); },
|
|
1324
1330
|
});
|
|
1325
1331
|
conversation = createVoiceConversation({
|
|
1326
1332
|
streamInput: (stream, callbacks) => createStreamingCapture(stream, { ...callbacks, request: (path, options) => this.runtime.voiceRequest(path, options), headers: conversation.headers, model: config.model, language: config.language, socket: (ticket) => this.runtime.voiceSocket(ticket, "/api/lares/voice/stream") }),
|
|
@@ -1350,6 +1356,7 @@ export default {
|
|
|
1350
1356
|
this.speakNotice = speakProblemText(this.t, err);
|
|
1351
1357
|
},
|
|
1352
1358
|
async toggleSpeak() {
|
|
1359
|
+
this.player?.unlock();
|
|
1353
1360
|
if (this.speakBusy) return;
|
|
1354
1361
|
this.speakNotice = "";
|
|
1355
1362
|
if (typeof this.runtime.settings?.saveVoice !== "function") {
|
|
@@ -1525,9 +1532,9 @@ export default {
|
|
|
1525
1532
|
async send(mode = "queue") {
|
|
1526
1533
|
const text = this.draft.trim();
|
|
1527
1534
|
if (!this.canSend) return;
|
|
1535
|
+
this.player?.unlock();
|
|
1528
1536
|
this.reader.stop();
|
|
1529
1537
|
if (this.voiceCall?.connection === "open") await this.voicePanel.pause();
|
|
1530
|
-
this.player?.unlock();
|
|
1531
1538
|
const sessionId = this.sessionId;
|
|
1532
1539
|
const images = this.draftImages.filter((row) => draftImageStatus(row) === "ready");
|
|
1533
1540
|
const wasRunning = this.running;
|
package/src/chat/Composer.vue
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<form class="lares-composer" :class="{ 'lares-composer--hero': hero }" @submit.prevent="onSend">
|
|
3
|
-
<div v-if="
|
|
3
|
+
<div v-if="callFailure" class="lares-composer__alert" role="alert">
|
|
4
|
+
<p>{{ callFailureText }}</p>
|
|
5
|
+
<button type="button" :disabled="voiceView.busy" @click="$emit('voice-action', { action: 'start' })">{{ t(callFailure.action === 'resume' ? 'call.resume' : 'call.restart') }}</button>
|
|
6
|
+
<button type="button" class="lares-composer__alert-quiet" :disabled="voiceView.busy" @click="$emit('voice-action', { action: 'end' })">{{ t('panel.close') }}</button>
|
|
7
|
+
</div>
|
|
8
|
+
<div v-else-if="voiceView?.readAloudError" class="lares-composer__alert" role="alert">
|
|
4
9
|
<p>{{ t("settings.readAloudSyncFailed") }}</p>
|
|
5
10
|
<button type="button" @click="$emit('voice-action', { action: 'retryReadAloud' })">{{ t("dictation.retry") }}</button>
|
|
6
11
|
</div>
|
|
@@ -164,6 +169,8 @@
|
|
|
164
169
|
</template>
|
|
165
170
|
|
|
166
171
|
<script>
|
|
172
|
+
import { messageFromCode } from "@olares/lares-core/i18n/t";
|
|
173
|
+
import { voiceCallFailure } from "@olares/lares-core/voice/presentation";
|
|
167
174
|
import LaresVoiceInput from "./VoiceInput.vue";
|
|
168
175
|
import {
|
|
169
176
|
REFERENCE_DEBOUNCE_MS,
|
|
@@ -242,6 +249,12 @@ export default {
|
|
|
242
249
|
overlayPointerActive() {
|
|
243
250
|
return this.referenceOpen || this.commandsOpen;
|
|
244
251
|
},
|
|
252
|
+
callFailure() {
|
|
253
|
+
return this.voiceView?.inputActive ? voiceCallFailure(this.voiceView) : null;
|
|
254
|
+
},
|
|
255
|
+
callFailureText() {
|
|
256
|
+
return this.callFailure ? messageFromCode(this.t, this.callFailure.code, "error.voice_failed") : "";
|
|
257
|
+
},
|
|
245
258
|
},
|
|
246
259
|
watch: {
|
|
247
260
|
draft(value) {
|
|
@@ -453,7 +466,8 @@ export default {
|
|
|
453
466
|
.lares-composer__alert {
|
|
454
467
|
display: flex;
|
|
455
468
|
align-items: center;
|
|
456
|
-
|
|
469
|
+
flex-wrap: wrap;
|
|
470
|
+
gap: 8px 12px;
|
|
457
471
|
padding: 8px 12px;
|
|
458
472
|
border-radius: 12px;
|
|
459
473
|
background: var(--q-background-1);
|
|
@@ -480,6 +494,11 @@ export default {
|
|
|
480
494
|
font-weight: 600;
|
|
481
495
|
}
|
|
482
496
|
|
|
497
|
+
.lares-composer__alert button.lares-composer__alert-quiet {
|
|
498
|
+
background: transparent;
|
|
499
|
+
color: var(--q-ink-2);
|
|
500
|
+
}
|
|
501
|
+
|
|
483
502
|
.lares-composer__card {
|
|
484
503
|
position: relative;
|
|
485
504
|
display: flex;
|
package/src/chat/VoiceInput.vue
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
<svg v-if="view.mode === 'single' && ui.recording && levels.length" class="lares-inline-level" viewBox="0 0 32 24" aria-hidden="true"><line v-for="(level, index) in levels" :key="index" :x1="index * 5 + 3" :x2="index * 5 + 3" :y1="12 - Math.max(2, Math.min(10, level * 500))" :y2="12 + Math.max(2, Math.min(10, level * 500))" /></svg>
|
|
7
7
|
<span>{{ t(ui.label) }}</span><span v-if="ui.timed" class="lares-inline-time" aria-hidden="true"> · {{ ui.elapsed }}</span>
|
|
8
8
|
</button>
|
|
9
|
-
<span class="lares-inline-status" :data-
|
|
9
|
+
<span v-if="!ui.error" class="lares-inline-status" :data-visible="['speaking', 'working'].includes(ui.phase)" role="status" aria-live="polite" aria-atomic="true">{{ status }}</span>
|
|
10
10
|
</div>
|
|
11
11
|
</template>
|
|
12
12
|
<script>
|
|
@@ -45,6 +45,5 @@ export default {
|
|
|
45
45
|
[data-phase=recording] .lares-inline-main { color: var(--q-blue-default, #3377ff); }
|
|
46
46
|
[data-phase=cancel] .lares-inline-main, .lares-inline-cancel[data-armed=true] { color: var(--q-red-default, #c23d32); }
|
|
47
47
|
.lares-inline-status { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); }
|
|
48
|
-
.lares-inline-status[data-visible=true]
|
|
49
|
-
.lares-inline-status[data-error=true] { color: var(--q-red-default, #c23d32); }
|
|
48
|
+
.lares-inline-status[data-visible=true] { width: auto; height: auto; clip-path: none; bottom: calc(100% + 16px); background: var(--q-background-1, white); padding: 6px; font-size: 12px; color: var(--q-ink-2, #777); }
|
|
50
49
|
</style>
|
package/src/chat/playback.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { primePlaybackContext } from "@olares/lares-core/voice/pcm-player";
|
|
2
|
+
|
|
1
3
|
const SILENT_WAV = "data:audio/wav;base64,UklGRiQAAABXQVZFZm10IBAAAAABAAEAQB8AAEAfAAABAAgAZGF0YQAAAAA=";
|
|
2
4
|
|
|
3
5
|
/**
|
|
@@ -20,6 +22,7 @@ export function createAnswerPlayer() {
|
|
|
20
22
|
};
|
|
21
23
|
return {
|
|
22
24
|
unlock() {
|
|
25
|
+
primePlaybackContext();
|
|
23
26
|
if (!audio || !audio.paused) return;
|
|
24
27
|
audio.src = SILENT_WAV;
|
|
25
28
|
void audio.play().catch(() => {});
|
|
@@ -15,7 +15,10 @@
|
|
|
15
15
|
@effort="$emit('open-model')"
|
|
16
16
|
@speak="$emit('toggle-speak')"
|
|
17
17
|
/>
|
|
18
|
-
<p v-if="state.speakNotice" class="lares-speak-notice" role="status">
|
|
18
|
+
<p v-if="state.speakNotice" class="lares-speak-notice" role="status">
|
|
19
|
+
<span>{{ state.speakNotice }}</span>
|
|
20
|
+
<button type="button" @click="$emit('dismiss-speak')">{{ t('readout.dismiss') }}</button>
|
|
21
|
+
</p>
|
|
19
22
|
<LaresHistoryPanel
|
|
20
23
|
:open="state.panel === 'history'"
|
|
21
24
|
:sessions="state.sessions"
|
|
@@ -268,6 +271,7 @@ export default {
|
|
|
268
271
|
"close-directory", "choose-directory", "create-folder", "toggle-hidden",
|
|
269
272
|
"rename-session", "archive-session",
|
|
270
273
|
"move-session",
|
|
274
|
+
"dismiss-speak",
|
|
271
275
|
],
|
|
272
276
|
data() {
|
|
273
277
|
return { composerMode: "text" };
|
|
@@ -340,12 +344,35 @@ export default {
|
|
|
340
344
|
}
|
|
341
345
|
|
|
342
346
|
.lares-speak-notice {
|
|
347
|
+
display: flex;
|
|
348
|
+
align-items: center;
|
|
349
|
+
gap: 12px;
|
|
343
350
|
margin: 0 16px 8px;
|
|
351
|
+
padding: 8px 12px;
|
|
352
|
+
border-radius: 12px;
|
|
353
|
+
background: var(--q-orange-alpha);
|
|
344
354
|
color: var(--q-orange-default);
|
|
345
355
|
font-size: 14px;
|
|
346
356
|
line-height: 20px;
|
|
347
357
|
}
|
|
348
358
|
|
|
359
|
+
.lares-speak-notice span {
|
|
360
|
+
flex: 1;
|
|
361
|
+
min-width: 0;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
.lares-speak-notice button {
|
|
365
|
+
flex: none;
|
|
366
|
+
border: 0;
|
|
367
|
+
border-radius: 8px;
|
|
368
|
+
padding: 6px 10px;
|
|
369
|
+
background: transparent;
|
|
370
|
+
color: var(--q-ink-2);
|
|
371
|
+
font: inherit;
|
|
372
|
+
font-size: 13px;
|
|
373
|
+
font-weight: 600;
|
|
374
|
+
}
|
|
375
|
+
|
|
349
376
|
.lares-shell__retry {
|
|
350
377
|
align-self: flex-start;
|
|
351
378
|
margin: 0 20px;
|