@olares/lares-mobile 0.15.24 → 0.15.26
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 +18 -4
- package/src/chat/ChatBar.vue +4 -0
- package/src/chat/Transcript.vue +11 -12
- package/src/chat/VoiceInput.vue +0 -5
- package/src/desktop/DesktopQueue.vue +1 -3
- package/src/desktop/DesktopShell.vue +1 -0
- package/src/host.js +2 -0
- package/src/mobile/MobileShell.vue +27 -1
- package/src/motion.css +16 -0
- package/src/settings/AgentSettings.vue +206 -36
- package/src/settings/Select.vue +7 -1
- package/src/settings/Sheet.vue +19 -0
- package/src/settings/TtsSettings.vue +260 -39
- package/src/settings/VoiceKeys.vue +267 -0
- package/src/settings/VoicePreferences.vue +80 -32
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@olares/lares-mobile",
|
|
3
|
-
"version": "0.15.
|
|
3
|
+
"version": "0.15.26",
|
|
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.26",
|
|
32
32
|
"markdown-it": "^14.3.0"
|
|
33
33
|
}
|
|
34
34
|
}
|
package/src/App.vue
CHANGED
|
@@ -124,6 +124,7 @@ import { failText, withPendingUser } from "./chat/format.js";
|
|
|
124
124
|
import { captureSource } from "@olares/lares-core/voice/input";
|
|
125
125
|
import { laresVoiceConfig, laresVoiceAvailability } from "@olares/lares-core/voice/lares-policy";
|
|
126
126
|
import { createVoiceConversation } from "@olares/lares-core/voice/conversation";
|
|
127
|
+
import { createUnifiedConversation } from "@olares/lares-core/voice/unified-conversation";
|
|
127
128
|
import { createAnswerPlayer } from "./chat/playback.js";
|
|
128
129
|
import { createReplySpeaker, createSentencePlayer, readoutState, speakable } from "@olares/lares-core/voice/speech";
|
|
129
130
|
import { prepareBrowserCapture } from "@olares/lares-core/voice/browser-capture";
|
|
@@ -213,6 +214,7 @@ export default {
|
|
|
213
214
|
draftBySession: {},
|
|
214
215
|
imagesBySession: {},
|
|
215
216
|
failed: "",
|
|
217
|
+
actionNotice: "",
|
|
216
218
|
error: "",
|
|
217
219
|
items: [],
|
|
218
220
|
pendingUser: "",
|
|
@@ -325,6 +327,7 @@ export default {
|
|
|
325
327
|
draft: this.draft,
|
|
326
328
|
failed: this.failed,
|
|
327
329
|
failText: this.failText,
|
|
330
|
+
actionNotice: this.actionNotice,
|
|
328
331
|
viewItems: this.viewItems,
|
|
329
332
|
historyLoading: this.historyLoading,
|
|
330
333
|
previews: this.previews,
|
|
@@ -577,6 +580,7 @@ export default {
|
|
|
577
580
|
});
|
|
578
581
|
this.voiceSettingsUnsub = subscribeVoiceSettings((value) => {
|
|
579
582
|
this.voiceDefaultMode = value.config.mobileMode; this.voiceSettings = markRaw(value);
|
|
583
|
+
this.voiceStreaming = value.config.streaming === true;
|
|
580
584
|
const next = value.config.speak === true;
|
|
581
585
|
if (this.voiceSpeak && !next) this.reader?.stop();
|
|
582
586
|
this.voiceSpeak = next;
|
|
@@ -704,6 +708,7 @@ export default {
|
|
|
704
708
|
this.noteSeen(this.sessionId);
|
|
705
709
|
this.noteSeen(snap.sessionId);
|
|
706
710
|
this.pendingUser = "";
|
|
711
|
+
this.actionNotice = "";
|
|
707
712
|
this.previews = {};
|
|
708
713
|
this.resetPreview();
|
|
709
714
|
this.commands = null;
|
|
@@ -726,7 +731,7 @@ export default {
|
|
|
726
731
|
this.running = snap.running;
|
|
727
732
|
this.runningSince = snap.runningSince || 0;
|
|
728
733
|
if (this.voiceCall) this.reader.stop();
|
|
729
|
-
const target = this.voiceSpeak && !this.voiceCall && !document.hidden ? speakable(utterance,
|
|
734
|
+
const target = this.voiceSpeak && !this.voiceCall && !document.hidden ? speakable(utterance, this.voiceStreaming) : null;
|
|
730
735
|
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}` });
|
|
731
736
|
this.failed = snap.failed;
|
|
732
737
|
this.error = snap.error;
|
|
@@ -1037,6 +1042,7 @@ export default {
|
|
|
1037
1042
|
this.resetPreview();
|
|
1038
1043
|
this.pendingUser = "";
|
|
1039
1044
|
this.failed = "";
|
|
1045
|
+
this.actionNotice = "";
|
|
1040
1046
|
this.error = "";
|
|
1041
1047
|
this.modelCatalog = null;
|
|
1042
1048
|
this.modelCatalogError = "";
|
|
@@ -1317,7 +1323,13 @@ export default {
|
|
|
1317
1323
|
const device = settings.config.device === "headset" ? "headset" : "device";
|
|
1318
1324
|
onDevice(device);
|
|
1319
1325
|
if (settings.config.backend === "realtime" && continuous) throw new Error("voice_manual_realtime_unavailable");
|
|
1320
|
-
this.reader.stop(); this.player?.stop();
|
|
1326
|
+
this.reader.stop(); this.player?.stop(); this.player?.unlock();
|
|
1327
|
+
if (config.transport !== "legacy") {
|
|
1328
|
+
return createUnifiedConversation({ request: (path, options) => this.runtime.voiceRequest(path, options), socket: (ticket) => this.runtime.voiceSocket(ticket),
|
|
1329
|
+
device: captureSource("mobile", config.device), readAloud: continuous && config.speak, preset: config.preset, presentationSession: true,
|
|
1330
|
+
endpointSilenceMs: config.endpointSilenceMs, idleTimeoutMs: config.idleTimeoutMs, recoveryTimeoutMs: config.recoveryTimeoutMs,
|
|
1331
|
+
onTranscript: continuous ? null : onTranscript, mode: () => resolveSubmitMode(this.running, false, true, this.busyEnter) });
|
|
1332
|
+
}
|
|
1321
1333
|
let conversation;
|
|
1322
1334
|
const reader = createSentencePlayer({
|
|
1323
1335
|
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") }),
|
|
@@ -1340,7 +1352,7 @@ export default {
|
|
|
1340
1352
|
onTranscript: continuous ? null : onTranscript,
|
|
1341
1353
|
mode: () => resolveSubmitMode(this.running, false, true, this.busyEnter),
|
|
1342
1354
|
playback: { get canPause() { return reader.canPause; }, get key() { return reader.key; }, pause: () => reader.pause(), resume: () => reader.resume(), stop: () => { reader.stop(); this.player.stop(); } },
|
|
1343
|
-
onReply: (reply) => { const value = speakable(reply,
|
|
1355
|
+
onReply: (reply) => { const value = speakable(reply, config.streaming === true); if (value) reader.follow(value); },
|
|
1344
1356
|
});
|
|
1345
1357
|
return conversation;
|
|
1346
1358
|
},
|
|
@@ -1584,10 +1596,12 @@ export default {
|
|
|
1584
1596
|
async updateQueue(itemId, action) {
|
|
1585
1597
|
if (!itemId || this.queueBusy) return;
|
|
1586
1598
|
this.queueBusy = itemId;
|
|
1599
|
+
this.actionNotice = "";
|
|
1587
1600
|
try {
|
|
1588
1601
|
const changed = await this.runtime.updateQueue(itemId, action);
|
|
1589
1602
|
if (!changed.ok) {
|
|
1590
|
-
|
|
1603
|
+
const detail = changed.error?.message || changed.error?.code || "queue";
|
|
1604
|
+
this.actionNotice = this.t("chat.failed", { error: detail });
|
|
1591
1605
|
}
|
|
1592
1606
|
} finally {
|
|
1593
1607
|
this.queueBusy = "";
|
package/src/chat/ChatBar.vue
CHANGED
package/src/chat/Transcript.vue
CHANGED
|
@@ -34,10 +34,6 @@
|
|
|
34
34
|
:data-type="row.type"
|
|
35
35
|
:data-pending="row.pending ? 'true' : 'false'"
|
|
36
36
|
>
|
|
37
|
-
<div v-if="hasReadout(row)" role="status" class="lares-message-readout">
|
|
38
|
-
<span>{{ speechLabel }}</span>
|
|
39
|
-
<button type="button" class="lares-message-stop" @click="readout.stop()">{{ t(readout.status === 'error' ? 'readout.dismiss' : 'readout.stop') }}</button>
|
|
40
|
-
</div>
|
|
41
37
|
<div v-if="row.type === 'user'" class="lares-msg" data-role="user">{{ row.text }}</div>
|
|
42
38
|
<LaresStageRow
|
|
43
39
|
v-else-if="row.type === 'context'"
|
|
@@ -75,16 +71,19 @@
|
|
|
75
71
|
:title="retryLabel(row)"
|
|
76
72
|
:body="retryBody(row)"
|
|
77
73
|
/>
|
|
78
|
-
<div
|
|
79
|
-
v-else-if="row.type === 'assistant' && row.pending"
|
|
80
|
-
class="lares-md"
|
|
81
|
-
data-pending="true"
|
|
82
|
-
>{{ row.text }}</div>
|
|
83
74
|
<template v-else-if="row.type === 'assistant'">
|
|
84
|
-
<div class="lares-md"
|
|
85
|
-
<
|
|
86
|
-
|
|
75
|
+
<div v-if="row.pending" class="lares-md" data-pending="true">{{ row.text }}</div>
|
|
76
|
+
<template v-else>
|
|
77
|
+
<div class="lares-md" v-html="renderMarkdown(row.text)" />
|
|
78
|
+
<p v-if="row.voiceSummary" class="lares-voice-summary">{{ row.voiceSummary }}</p>
|
|
79
|
+
</template>
|
|
80
|
+
<div v-if="hasReadout(row)" role="status" class="lares-message-readout">
|
|
81
|
+
<span>{{ speechLabel }}</span>
|
|
82
|
+
<button type="button" class="lares-message-stop" @click="readout.stop()">{{ t(readout.status === 'error' ? 'readout.dismiss' : 'readout.stop') }}</button>
|
|
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>
|
|
87
85
|
<LaresMessageActions
|
|
86
|
+
v-if="!row.pending"
|
|
88
87
|
:text="row.text"
|
|
89
88
|
:reaction="reactions[actionKey(index)] || ''"
|
|
90
89
|
:metrics="row.metrics || null"
|
package/src/chat/VoiceInput.vue
CHANGED
|
@@ -6,20 +6,17 @@
|
|
|
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 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
9
|
</div>
|
|
11
10
|
</template>
|
|
12
11
|
<script>
|
|
13
12
|
import { createHoldGesture } from '@olares/lares-core/voice/hold';
|
|
14
13
|
import { voiceInputPresentation } from '@olares/lares-core/voice/presentation';
|
|
15
|
-
import { messageFromCode } from '@olares/lares-core/i18n/t';
|
|
16
14
|
export default {
|
|
17
15
|
name: 'LaresVoiceInput', props: { view: { type: Object, required: true }, t: { type: Function, required: true } }, emits: ['action'],
|
|
18
16
|
data: () => ({ now: Date.now(), clock: null, gesture: null }),
|
|
19
17
|
computed: {
|
|
20
18
|
ui() { return voiceInputPresentation(this.view, this.now); },
|
|
21
19
|
levels() { return (this.view.levels || []).slice(-6); },
|
|
22
|
-
status() { return this.ui.error ? messageFromCode(this.t, this.ui.error, 'error.voice_failed') : this.t(this.ui.status); },
|
|
23
20
|
},
|
|
24
21
|
created() { this.gesture = createHoldGesture({ start: () => this.action('holdStart'), end: value => this.action('holdEnd', value), onCancelChange: value => this.action('setCancelArmed', value), isActive: () => this.view.holding }); },
|
|
25
22
|
mounted() { this.$refs.main?.focus(); this.clock = setInterval(() => { this.now = Date.now(); }, 250); window.addEventListener('blur', this.cancel); window.addEventListener('pointerdown', this.additionalPointer, true); },
|
|
@@ -44,6 +41,4 @@ export default {
|
|
|
44
41
|
.lares-inline-cancel { position: absolute; bottom: calc(100% + 18px); left: 50%; transform: translateX(-50%); white-space: nowrap; font-size: 13px; padding: 7px 12px; border-radius: 10px; background: var(--q-background-1, white); box-shadow: 0 2px 12px #0001; pointer-events: none; }
|
|
45
42
|
[data-phase=recording] .lares-inline-main { color: var(--q-blue-default, #3377ff); }
|
|
46
43
|
[data-phase=cancel] .lares-inline-main, .lares-inline-cancel[data-armed=true] { color: var(--q-red-default, #c23d32); }
|
|
47
|
-
.lares-inline-status { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); }
|
|
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); }
|
|
49
44
|
</style>
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
|
|
3
|
-
<div v-if="rows.length" class="desktop-queue">
|
|
2
|
+
<div v-if="rows.length" class="desktop-queue lares-queue-in">
|
|
4
3
|
<div class="desktop-queue__panel">
|
|
5
4
|
<button
|
|
6
5
|
v-if="rows.length > 1"
|
|
@@ -70,7 +69,6 @@
|
|
|
70
69
|
</ul>
|
|
71
70
|
</div>
|
|
72
71
|
</div>
|
|
73
|
-
</Transition>
|
|
74
72
|
</template>
|
|
75
73
|
|
|
76
74
|
<script>
|
|
@@ -35,6 +35,7 @@
|
|
|
35
35
|
<span>{{ state.failText }}</span>
|
|
36
36
|
<button type="button" :disabled="state.starting" @click="$emit('retry')">{{ t("probe.retry") }}</button>
|
|
37
37
|
</div>
|
|
38
|
+
<p v-else-if="state.actionNotice" class="desktop-shell__failure" role="status">{{ state.actionNotice }}</p>
|
|
38
39
|
<div v-if="newSessionHero" class="desktop-shell__brand">
|
|
39
40
|
<div class="desktop-shell__logo" :style="logoStyle" aria-hidden="true" />
|
|
40
41
|
<h1>{{ t("shell.title") }}</h1>
|
package/src/host.js
CHANGED
|
@@ -5,6 +5,7 @@ import { normalizeReferenceCandidates, referencesUrl } from "@olares/lares-core/
|
|
|
5
5
|
import { muxWsUrl, remoteStreamOpen, workspaceBaseline } from "@olares/lares-core/larepass/mux";
|
|
6
6
|
import { RESPOND_PATH } from "@olares/lares-core/larepass/rpc";
|
|
7
7
|
import { createHostSettings } from "@olares/lares-core/larepass/settings";
|
|
8
|
+
import { createVoiceKeysClient } from "@olares/lares-core/voice/keys-client";
|
|
8
9
|
import {
|
|
9
10
|
downloadFileUrl,
|
|
10
11
|
isPreviewPayload,
|
|
@@ -340,5 +341,6 @@ export function createHostClient(ports = {}) {
|
|
|
340
341
|
},
|
|
341
342
|
consumeMux,
|
|
342
343
|
settings: createHostSettings(request),
|
|
344
|
+
voiceKeys: createVoiceKeysClient((path, init) => request(path, init)),
|
|
343
345
|
};
|
|
344
346
|
}
|
|
@@ -51,6 +51,7 @@
|
|
|
51
51
|
/>
|
|
52
52
|
<div class="lares-shell__conversation" :data-hero="newSessionHero ? 'true' : 'false'">
|
|
53
53
|
<p v-if="state.failed" class="lares-shell__status">{{ state.failText }}</p>
|
|
54
|
+
<p v-else-if="state.actionNotice" class="lares-shell__status" role="status">{{ state.actionNotice }}</p>
|
|
54
55
|
<button
|
|
55
56
|
v-if="state.failed"
|
|
56
57
|
type="button"
|
|
@@ -176,7 +177,6 @@
|
|
|
176
177
|
|
|
177
178
|
<LaresSheet :open="state.modelSheet" :title="t('model.menuAria')" @close="$emit('close-model')">
|
|
178
179
|
<p v-if="state.modelCatalogError" class="lares-shell__status" role="status">{{ state.modelCatalogError }}</p>
|
|
179
|
-
<button type="button" @click="state.refreshModels()">{{ t('agent.refresh') }}</button>
|
|
180
180
|
<p v-if="state.modelError" class="lares-shell__status">{{ state.modelError }}</p>
|
|
181
181
|
<p v-else-if="!state.models" class="lares-shell__hint">{{ t("agent.loading") }}</p>
|
|
182
182
|
<p v-else-if="!state.modelGroups.length" class="lares-shell__hint">{{ t("model.empty") }}</p>
|
|
@@ -202,6 +202,9 @@
|
|
|
202
202
|
@click="$emit('choose-effort', row.id)"
|
|
203
203
|
/>
|
|
204
204
|
</template>
|
|
205
|
+
<template #footer>
|
|
206
|
+
<button type="button" class="lares-shell__refresh" @click="state.refreshModels()">{{ t('agent.refresh') }}</button>
|
|
207
|
+
</template>
|
|
205
208
|
</LaresSheet>
|
|
206
209
|
<Transition name="lares-preview">
|
|
207
210
|
<LaresPreview
|
|
@@ -383,6 +386,29 @@ export default {
|
|
|
383
386
|
color: var(--q-blue-default);
|
|
384
387
|
}
|
|
385
388
|
|
|
389
|
+
.lares-shell__refresh {
|
|
390
|
+
display: flex;
|
|
391
|
+
flex: none;
|
|
392
|
+
align-items: center;
|
|
393
|
+
justify-content: center;
|
|
394
|
+
width: 100%;
|
|
395
|
+
height: 48px;
|
|
396
|
+
margin: 0;
|
|
397
|
+
border: 0;
|
|
398
|
+
border-radius: 0;
|
|
399
|
+
padding: 0;
|
|
400
|
+
background: var(--q-yellow-default);
|
|
401
|
+
color: var(--q-ink-1);
|
|
402
|
+
font: inherit;
|
|
403
|
+
font-size: 16px;
|
|
404
|
+
font-weight: 500;
|
|
405
|
+
text-align: center;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
.lares-shell__refresh:active {
|
|
409
|
+
background: var(--q-yellow-pressed);
|
|
410
|
+
}
|
|
411
|
+
|
|
386
412
|
.lares-shell__heading {
|
|
387
413
|
margin: 0;
|
|
388
414
|
padding: 12px 16px 4px;
|
package/src/motion.css
CHANGED
|
@@ -101,6 +101,20 @@
|
|
|
101
101
|
.lares-panel-leave-to {
|
|
102
102
|
opacity: 0;
|
|
103
103
|
transform: translateY(8px);
|
|
104
|
+
pointer-events: none;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/* A panel that is still arriving or leaving is not a click target. */
|
|
108
|
+
.lares-panel-leave-active {
|
|
109
|
+
pointer-events: none;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
@keyframes lares-queue-in {
|
|
113
|
+
from { opacity: 0; transform: translateY(8px); }
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
.lares-queue-in {
|
|
117
|
+
animation: lares-queue-in var(--lares-duration-normal, 220ms) var(--lares-ease-out, cubic-bezier(0.32, 0.72, 0, 1));
|
|
104
118
|
}
|
|
105
119
|
|
|
106
120
|
/* Preview tab strip — slide in from the header edge. */
|
|
@@ -151,6 +165,8 @@
|
|
|
151
165
|
transition: none;
|
|
152
166
|
}
|
|
153
167
|
|
|
168
|
+
.lares-queue-in { animation: none; }
|
|
169
|
+
|
|
154
170
|
.lares-preview-enter-from,
|
|
155
171
|
.lares-preview-leave-to,
|
|
156
172
|
.lares-view-enter-from,
|