@olares/lares-mobile 0.15.21 → 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 +13 -10
- package/src/chat/Composer.vue +53 -0
- package/src/chat/VoiceInput.vue +2 -3
- package/src/chat/playback.js +3 -0
- package/src/desktop/DesktopComposer.vue +7 -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,11 +101,8 @@
|
|
|
101
101
|
@open-references="loadReferences"
|
|
102
102
|
@pick-command="pickCommand"
|
|
103
103
|
@toggle-speak="toggleSpeak"
|
|
104
|
+
@dismiss-speak="speakNotice = ''"
|
|
104
105
|
/>
|
|
105
|
-
<div v-if="voicePanelState?.readAloudError" class="lares-speak-notice" role="alert">
|
|
106
|
-
{{ voiceReadAloudError }}
|
|
107
|
-
<button type="button" @click="voicePanel.retryReadAloud()">{{ t('dictation.retry') }}</button>
|
|
108
|
-
</div>
|
|
109
106
|
<LaresVoicePanel v-if="voicePanelState" :view="voicePanelState" :t="t"
|
|
110
107
|
@close="runVoicePanel('close')" @edit="voicePanel.edit($event)" @preset="voicePanel.selectPreset($event, true).catch(() => {})"
|
|
111
108
|
@complete="runVoicePanel('complete')" @discard="runVoicePanel('discard')" />
|
|
@@ -128,7 +125,7 @@ import { captureSource } from "@olares/lares-core/voice/input";
|
|
|
128
125
|
import { laresVoiceConfig, laresVoiceAvailability } from "@olares/lares-core/voice/lares-policy";
|
|
129
126
|
import { createVoiceConversation } from "@olares/lares-core/voice/conversation";
|
|
130
127
|
import { createAnswerPlayer } from "./chat/playback.js";
|
|
131
|
-
import { createReplySpeaker, createSentencePlayer, speakable } from "@olares/lares-core/voice/speech";
|
|
128
|
+
import { createReplySpeaker, createSentencePlayer, readoutState, speakable } from "@olares/lares-core/voice/speech";
|
|
132
129
|
import { prepareBrowserCapture } from "@olares/lares-core/voice/browser-capture";
|
|
133
130
|
import { playAudioStream, createStreamingCapture } from "@olares/lares-core/voice/stream-client";
|
|
134
131
|
import { voiceHeaders, wavBlob } from "@olares/lares-core/voice/client";
|
|
@@ -534,7 +531,6 @@ export default {
|
|
|
534
531
|
modelBlocked() {
|
|
535
532
|
return !modelState(this.modelStatus).valid;
|
|
536
533
|
},
|
|
537
|
-
voiceReadAloudError() { return this.t("settings.readAloudSyncFailed"); },
|
|
538
534
|
voiceErrorText() {
|
|
539
535
|
return this.voiceError ? messageFromCode(this.t, this.voiceError, "error.voice_failed") : "";
|
|
540
536
|
},
|
|
@@ -563,6 +559,7 @@ export default {
|
|
|
563
559
|
this.imageUploadControllers = new Map();
|
|
564
560
|
this.player = createAnswerPlayer();
|
|
565
561
|
this.voicePanel = createVoicePanel({
|
|
562
|
+
armOutput: () => this.player?.unlock(),
|
|
566
563
|
create: (options) => this.createMobileVoiceConversation(options),
|
|
567
564
|
prepareInput: (config) => config?.device === "headset" ? null : prepareBrowserCapture(),
|
|
568
565
|
loadConfig: async () => (await this.runtime.settings.voice(false, { diagnostics: false })).config,
|
|
@@ -1280,6 +1277,7 @@ export default {
|
|
|
1280
1277
|
await this.openDictation();
|
|
1281
1278
|
},
|
|
1282
1279
|
async openDictation(preset) {
|
|
1280
|
+
this.voicePanel.arm();
|
|
1283
1281
|
if (document.hidden || navigator.onLine === false) return;
|
|
1284
1282
|
if (!this.voiceSettings) {
|
|
1285
1283
|
try { this.voiceSettings = markRaw(await this.runtime.settings.voice(false, { diagnostics: false })); }
|
|
@@ -1293,6 +1291,7 @@ export default {
|
|
|
1293
1291
|
await this.voicePanel.open({ mode: "single", sessionId: this.sessionId || null, preset: preset && preset !== "default" ? preset : undefined, configSnapshot: this.voiceSettings.config }).catch(() => {});
|
|
1294
1292
|
},
|
|
1295
1293
|
async onHoldStart() {
|
|
1294
|
+
this.voicePanel.arm();
|
|
1296
1295
|
if (this.voicePanelState?.mode === "single" && this.voicePanelState.raw) {
|
|
1297
1296
|
await this.voicePanel.open({ mode: "single", sessionId: this.sessionId || null });
|
|
1298
1297
|
return;
|
|
@@ -1304,9 +1303,11 @@ export default {
|
|
|
1304
1303
|
await this.voicePanel.open({ mode: this.voiceDefaultMode, sessionId: this.sessionId || null });
|
|
1305
1304
|
},
|
|
1306
1305
|
onVoiceAction({ action, value }) {
|
|
1306
|
+
if (action === "start" || action === "end" || action === "close") this.speakNotice = "";
|
|
1307
|
+
if (action === "retryReadAloud") { void this.voicePanel.retryReadAloud(); return; }
|
|
1307
1308
|
if (action === "stopPlayback") { void this.voicePanel.conversation?.controller.stopPlayback().catch(() => {}); return; }
|
|
1308
1309
|
if (action === "setCancelArmed") { this.voicePanel.setCancelArmed(value); return; }
|
|
1309
|
-
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(() => {});
|
|
1310
1311
|
},
|
|
1311
1312
|
runVoicePanel(action) { void this.voicePanel[action]().catch(() => {}); },
|
|
1312
1313
|
async createMobileVoiceConversation({ mode, onTranscript, onDevice, config }) {
|
|
@@ -1316,7 +1317,7 @@ export default {
|
|
|
1316
1317
|
const device = settings.config.device === "headset" ? "headset" : "device";
|
|
1317
1318
|
onDevice(device);
|
|
1318
1319
|
if (settings.config.backend === "realtime" && continuous) throw new Error("voice_manual_realtime_unavailable");
|
|
1319
|
-
this.reader.stop(); this.player?.stop();
|
|
1320
|
+
this.reader.stop(); this.player?.stop();
|
|
1320
1321
|
let conversation;
|
|
1321
1322
|
const reader = createSentencePlayer({
|
|
1322
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") }),
|
|
@@ -1324,7 +1325,8 @@ export default {
|
|
|
1324
1325
|
play: (sentence) => sentence.bluetooth === "played" ? Promise.resolve() : this.player.play(wavBlob(sentence.audio)),
|
|
1325
1326
|
halt: () => this.player.stop(), silence: () => {},
|
|
1326
1327
|
onState: (playing) => conversation?.playbackState(playing),
|
|
1327
|
-
|
|
1328
|
+
onOutput: (value) => readoutState.publish(value?.status === "error" ? null : value),
|
|
1329
|
+
fail: (error) => { void conversation.controller.disconnect(error?.code || error?.message || "voice_playback_failed"); },
|
|
1328
1330
|
});
|
|
1329
1331
|
conversation = createVoiceConversation({
|
|
1330
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") }),
|
|
@@ -1354,6 +1356,7 @@ export default {
|
|
|
1354
1356
|
this.speakNotice = speakProblemText(this.t, err);
|
|
1355
1357
|
},
|
|
1356
1358
|
async toggleSpeak() {
|
|
1359
|
+
this.player?.unlock();
|
|
1357
1360
|
if (this.speakBusy) return;
|
|
1358
1361
|
this.speakNotice = "";
|
|
1359
1362
|
if (typeof this.runtime.settings?.saveVoice !== "function") {
|
|
@@ -1529,9 +1532,9 @@ export default {
|
|
|
1529
1532
|
async send(mode = "queue") {
|
|
1530
1533
|
const text = this.draft.trim();
|
|
1531
1534
|
if (!this.canSend) return;
|
|
1535
|
+
this.player?.unlock();
|
|
1532
1536
|
this.reader.stop();
|
|
1533
1537
|
if (this.voiceCall?.connection === "open") await this.voicePanel.pause();
|
|
1534
|
-
this.player?.unlock();
|
|
1535
1538
|
const sessionId = this.sessionId;
|
|
1536
1539
|
const images = this.draftImages.filter((row) => draftImageStatus(row) === "ready");
|
|
1537
1540
|
const wasRunning = this.running;
|
package/src/chat/Composer.vue
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<form class="lares-composer" :class="{ 'lares-composer--hero': hero }" @submit.prevent="onSend">
|
|
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">
|
|
9
|
+
<p>{{ t("settings.readAloudSyncFailed") }}</p>
|
|
10
|
+
<button type="button" @click="$emit('voice-action', { action: 'retryReadAloud' })">{{ t("dictation.retry") }}</button>
|
|
11
|
+
</div>
|
|
3
12
|
<div
|
|
4
13
|
class="lares-composer__card"
|
|
5
14
|
data-composer-card
|
|
@@ -160,6 +169,8 @@
|
|
|
160
169
|
</template>
|
|
161
170
|
|
|
162
171
|
<script>
|
|
172
|
+
import { messageFromCode } from "@olares/lares-core/i18n/t";
|
|
173
|
+
import { voiceCallFailure } from "@olares/lares-core/voice/presentation";
|
|
163
174
|
import LaresVoiceInput from "./VoiceInput.vue";
|
|
164
175
|
import {
|
|
165
176
|
REFERENCE_DEBOUNCE_MS,
|
|
@@ -238,6 +249,12 @@ export default {
|
|
|
238
249
|
overlayPointerActive() {
|
|
239
250
|
return this.referenceOpen || this.commandsOpen;
|
|
240
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
|
+
},
|
|
241
258
|
},
|
|
242
259
|
watch: {
|
|
243
260
|
draft(value) {
|
|
@@ -446,6 +463,42 @@ export default {
|
|
|
446
463
|
padding: 0;
|
|
447
464
|
}
|
|
448
465
|
|
|
466
|
+
.lares-composer__alert {
|
|
467
|
+
display: flex;
|
|
468
|
+
align-items: center;
|
|
469
|
+
flex-wrap: wrap;
|
|
470
|
+
gap: 8px 12px;
|
|
471
|
+
padding: 8px 12px;
|
|
472
|
+
border-radius: 12px;
|
|
473
|
+
background: var(--q-background-1);
|
|
474
|
+
color: var(--q-orange-default);
|
|
475
|
+
font-size: 13px;
|
|
476
|
+
line-height: 18px;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
.lares-composer__alert p {
|
|
480
|
+
flex: 1;
|
|
481
|
+
min-width: 0;
|
|
482
|
+
margin: 0;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
.lares-composer__alert button {
|
|
486
|
+
flex: none;
|
|
487
|
+
border: 0;
|
|
488
|
+
border-radius: 8px;
|
|
489
|
+
padding: 6px 10px;
|
|
490
|
+
background: var(--q-blue-alpha);
|
|
491
|
+
color: var(--q-blue-default);
|
|
492
|
+
font: inherit;
|
|
493
|
+
font-size: 13px;
|
|
494
|
+
font-weight: 600;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
.lares-composer__alert button.lares-composer__alert-quiet {
|
|
498
|
+
background: transparent;
|
|
499
|
+
color: var(--q-ink-2);
|
|
500
|
+
}
|
|
501
|
+
|
|
449
502
|
.lares-composer__card {
|
|
450
503
|
position: relative;
|
|
451
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(() => {});
|
|
@@ -133,6 +133,10 @@
|
|
|
133
133
|
:t="t"
|
|
134
134
|
@action="(itemId, action) => $emit('queue-action', itemId, action)"
|
|
135
135
|
/>
|
|
136
|
+
<div v-if="state.voicePanelState?.readAloudError" class="desktop-composer__alert" role="alert">
|
|
137
|
+
<p>{{ t("settings.readAloudSyncFailed") }}</p>
|
|
138
|
+
<button type="button" @click="$emit('voice-action', { action: 'retryReadAloud' })">{{ t("dictation.retry") }}</button>
|
|
139
|
+
</div>
|
|
136
140
|
|
|
137
141
|
<Transition name="lares-panel" mode="out-in">
|
|
138
142
|
<div v-if="state.approval" key="approval" class="desktop-composer__approval">
|
|
@@ -910,6 +914,9 @@ export default {
|
|
|
910
914
|
margin: 0 auto;
|
|
911
915
|
padding: 8px 0 20px;
|
|
912
916
|
}
|
|
917
|
+
.desktop-composer__alert { display:flex; align-items:center; gap:12px; margin:0 0 8px; padding:8px 12px; border-radius:12px; background:var(--q-background-1); color:var(--q-orange-default); font-size:13px; line-height:18px; }
|
|
918
|
+
.desktop-composer__alert p { flex:1; min-width:0; margin:0; }
|
|
919
|
+
.desktop-composer__alert button { flex:none; border:0; border-radius:8px; padding:6px 10px; background:var(--q-blue-alpha); color:var(--q-blue-default); font:inherit; font-size:13px; font-weight:600; cursor:pointer; }
|
|
913
920
|
.desktop-composer__model-row { display:flex; justify-content:flex-end; gap:2px; min-height:28px; margin-bottom:6px; }
|
|
914
921
|
.desktop-composer[data-hero="true"] .desktop-composer__model-row { justify-content:space-between; }
|
|
915
922
|
.desktop-composer__model,
|
|
@@ -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;
|