@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.
@@ -44,36 +44,107 @@
44
44
  <section class="lares-agent__group">
45
45
  <h2>{{ tVoice("settings.title") }}</h2>
46
46
  <div class="lares-agent__list">
47
- <LaresSettingRow
48
- :label="tVoice('settings.stt.model')"
49
- :value="voiceModelLabel"
50
- chevron
51
- boxed
52
- :disabled="Boolean(pending)"
53
- @click="openSheet('voiceModel')"
54
- />
55
- <LaresSettingRow
56
- :label="tVoice('settings.language.title')"
57
- :value="voiceLangLabel"
58
- chevron
59
- boxed
60
- :disabled="Boolean(pending)"
61
- @click="openSheet('voiceLang')"
62
- />
47
+ <template v-if="isDesktop">
48
+ <label class="lares-agent__field">
49
+ <span>{{ tVoice('settings.stt.model') }}</span>
50
+ <LaresSelect
51
+ :value="voiceModelId"
52
+ :sections="voiceModelSections"
53
+ :disabled="busy || !voice"
54
+ @select="chooseVoiceModel"
55
+ />
56
+ </label>
57
+ <label class="lares-agent__field">
58
+ <span>{{ tVoice('settings.language.title') }}</span>
59
+ <LaresSelect
60
+ :value="voiceLangId"
61
+ :sections="voiceLanguageSections"
62
+ :disabled="busy || !voice"
63
+ @select="chooseVoiceLanguage"
64
+ />
65
+ </label>
66
+ </template>
67
+ <template v-else>
68
+ <LaresSettingRow
69
+ :label="tVoice('settings.stt.model')"
70
+ :value="voiceModelLabel"
71
+ chevron
72
+ boxed
73
+ :disabled="Boolean(pending)"
74
+ @click="openSheet('voiceModel')"
75
+ />
76
+ <LaresSettingRow
77
+ :label="tVoice('settings.language.title')"
78
+ :value="voiceLangLabel"
79
+ chevron
80
+ boxed
81
+ :disabled="Boolean(pending)"
82
+ @click="openSheet('voiceLang')"
83
+ />
84
+ </template>
63
85
  </div>
64
86
  <VoicePreferences :key="hostKey" ref="voicePreferences" :settings="settings" :tts-fetch="ttsFetch" :snapshot="voice" :device="device" :t="tVoice" :bind-before-leave="bindBeforeLeave" @change="voice = $event" />
65
- <label class="lares-agent__hint">{{ tVoice('settings.mobileMode') }}
66
- <select :value="voice?.config?.mobileMode || 'continuous'" @change="patchVoice('mobileMode', $event.target.value)">
67
- <option value="single">{{ tVoice('settings.mode.single') }}</option>
68
- <option value="continuous">{{ tVoice('settings.mode.continuous') }}</option>
69
- </select>
70
- </label>
71
- <label class="lares-agent__hint">{{ tVoice('settings.device') }}
72
- <select :value="voice?.config?.device || 'auto'" @change="patchVoice('device', $event.target.value)">
73
- <option v-for="device in ['auto', 'device', 'headset']" :key="device" :value="device">{{ tVoice(`settings.device.${device}`) }}</option>
74
- </select>
75
- </label>
87
+ <h3>{{ tVoice("settings.conversation") }}</h3>
88
+ <div class="lares-agent__list">
89
+ <template v-if="isDesktop">
90
+ <label class="lares-agent__field">
91
+ <span>{{ tVoice('settings.mobileMode') }}</span>
92
+ <LaresSelect
93
+ :value="voice?.config?.mobileMode || 'continuous'"
94
+ :sections="mobileModeSections"
95
+ :disabled="busy || !voice"
96
+ @select="(item) => patchVoice('mobileMode', item.id)"
97
+ />
98
+ </label>
99
+ <label class="lares-agent__field">
100
+ <span>{{ tVoice('settings.device') }}</span>
101
+ <LaresSelect
102
+ :value="voice?.config?.device || 'auto'"
103
+ :sections="deviceSections"
104
+ :disabled="busy || !voice"
105
+ @select="(item) => patchVoice('device', item.id)"
106
+ />
107
+ </label>
108
+ <label class="lares-agent__field">
109
+ <span>{{ tVoice('settings.mode.title') }}</span>
110
+ <LaresSelect
111
+ :value="voice?.config?.streaming ? 'streaming' : 'turns'"
112
+ :sections="readingSections"
113
+ :disabled="busy || !voice || !reading"
114
+ @select="(item) => chooseReading(item.id)"
115
+ />
116
+ </label>
117
+ </template>
118
+ <template v-else>
119
+ <LaresSettingRow
120
+ :label="tVoice('settings.mobileMode')"
121
+ :value="voiceChoiceLabel('mobileMode')"
122
+ chevron
123
+ boxed
124
+ :disabled="Boolean(pending) || !voice"
125
+ @click="openSheet('voiceMobileMode')"
126
+ />
127
+ <LaresSettingRow
128
+ :label="tVoice('settings.device')"
129
+ :value="voiceChoiceLabel('device')"
130
+ chevron
131
+ boxed
132
+ :disabled="Boolean(pending) || !voice"
133
+ @click="openSheet('voiceDevice')"
134
+ />
135
+ <LaresSettingRow
136
+ :label="tVoice('settings.mode.title')"
137
+ :value="voiceChoiceLabel('reading')"
138
+ chevron
139
+ boxed
140
+ :disabled="Boolean(pending) || !voice || !reading"
141
+ @click="openSheet('voiceReading')"
142
+ />
143
+ </template>
144
+ </div>
145
+ <p class="lares-agent__hint">{{ readingHint }}</p>
76
146
  <p v-if="voiceError" class="lares-agent__hint" data-status="error">{{ voiceError }}</p>
147
+ <VoiceKeys :key="`keys-${hostKey}`" :client="voiceKeys" :t="tVoice" />
77
148
  </section>
78
149
  <section class="lares-agent__group">
79
150
  <h2>{{ tSearch("settings.title") }}</h2>
@@ -105,7 +176,7 @@
105
176
  :key="item.id"
106
177
  :label="item.label"
107
178
  :checked="item.id === sheetCurrent"
108
- :disabled="busy"
179
+ :disabled="busy || item.disabled"
109
180
  @click="pick(item)"
110
181
  />
111
182
  </template>
@@ -138,6 +209,7 @@ import { adoptHost } from "../runtime.js";
138
209
  import { createT } from "../i18n.js";
139
210
  import { chatDevice, DEVICE_DESKTOP } from "../layout.js";
140
211
  import VoicePreferences from "./VoicePreferences.vue";
212
+ import VoiceKeys from "./VoiceKeys.vue";
141
213
  import LaresSettingRow from "./SettingRow.vue";
142
214
  import LaresSheet from "./Sheet.vue";
143
215
  import LaresSelect from "./Select.vue";
@@ -154,7 +226,7 @@ function itemLabel(items, id) {
154
226
 
155
227
  export default {
156
228
  name: "LaresAgentSettings",
157
- components: { VoicePreferences, LaresSettingRow, LaresSheet, LaresSelect },
229
+ components: { VoicePreferences, VoiceKeys, LaresSettingRow, LaresSheet, LaresSelect },
158
230
  expose: ["reload", "beforeLeave"],
159
231
  props: {
160
232
  locale: { type: String, default: "en" },
@@ -180,6 +252,7 @@ export default {
180
252
  voiceError: "",
181
253
  searchError: "",
182
254
  conversationError: "",
255
+ reading: null,
183
256
  };
184
257
  },
185
258
  computed: {
@@ -205,6 +278,7 @@ export default {
205
278
  return hostSessionKey(this.ports);
206
279
  },
207
280
  ttsFetch() { return createHostClient(this.ports).ttsFetch; },
281
+ voiceKeys() { return createHostClient(this.ports).voiceKeys; },
208
282
  settings() {
209
283
  return createHostClient(this.ports).settings;
210
284
  },
@@ -244,13 +318,31 @@ export default {
244
318
  }
245
319
  return this.models.default?.model || "—";
246
320
  },
321
+ voiceModelId() {
322
+ return voiceMenuValue(this.voice?.config?.model);
323
+ },
324
+ voiceLangId() {
325
+ return voiceMenuValue(this.voice?.config?.language);
326
+ },
327
+ voiceModelSections() {
328
+ return [{ items: this.voiceModels }];
329
+ },
330
+ voiceLanguageSections() {
331
+ return [{ items: this.voiceLanguages }];
332
+ },
247
333
  voiceModelLabel() {
248
334
  if (!this.voice) return "—";
249
- return itemLabel(this.voiceModels, voiceMenuValue(this.voice.config?.model));
335
+ return itemLabel(this.voiceModels, this.voiceModelId);
250
336
  },
251
337
  voiceLangLabel() {
252
338
  if (!this.voice) return "—";
253
- return itemLabel(this.voiceLanguages, voiceMenuValue(this.voice.config?.language));
339
+ return itemLabel(this.voiceLanguages, this.voiceLangId);
340
+ },
341
+ readingHint() {
342
+ if (this.reading && !this.reading.allowed) return this.tVoice("settings.mode.hint");
343
+ const type = this.reading?.gpu?.type;
344
+ if (type) return this.tVoice("settings.mode.gpu", { type: this.tVoice(`settings.gpu.${type}`) });
345
+ return this.tVoice("settings.mode.hint");
254
346
  },
255
347
  searchMenuId() {
256
348
  return searchMenuValue(this.search);
@@ -280,10 +372,27 @@ export default {
280
372
  ],
281
373
  }];
282
374
  },
375
+ mobileModeSections() {
376
+ return [{ items: ["single", "continuous"].map((id) => ({ id, label: this.tVoice(`settings.mode.${id}`) })) }];
377
+ },
378
+ deviceSections() {
379
+ return [{ items: ["auto", "device", "headset"].map((id) => ({ id, label: this.tVoice(`settings.device.${id}`) })) }];
380
+ },
381
+ readingSections() {
382
+ return [{
383
+ items: [
384
+ { id: "turns", label: this.tVoice("settings.mode.turns") },
385
+ { id: "streaming", label: this.tVoice("settings.mode.streaming"), disabled: !this.reading?.allowed },
386
+ ],
387
+ }];
388
+ },
283
389
  sheetTitle() {
284
390
  if (this.panel === "model") return this.tModel("settings.title");
285
391
  if (this.panel === "voiceModel") return this.tVoice("settings.stt.model");
286
392
  if (this.panel === "voiceLang") return this.tVoice("settings.language.title");
393
+ if (this.panel === "voiceMobileMode") return this.tVoice("settings.mobileMode");
394
+ if (this.panel === "voiceDevice") return this.tVoice("settings.device");
395
+ if (this.panel === "voiceReading") return this.tVoice("settings.mode.title");
287
396
  if (this.panel === "search") return this.tSearch("settings.default");
288
397
  return "";
289
398
  },
@@ -298,6 +407,9 @@ export default {
298
407
  if (this.panel === "voiceLang") {
299
408
  return [{ items: this.voiceLanguages.map((item) => ({ id: item.id, label: item.label })) }];
300
409
  }
410
+ if (this.panel === "voiceMobileMode") return this.mobileModeSections;
411
+ if (this.panel === "voiceDevice") return this.deviceSections;
412
+ if (this.panel === "voiceReading") return this.readingSections;
301
413
  if (this.panel === "search") return this.searchSelectSections;
302
414
  return [];
303
415
  },
@@ -357,7 +469,16 @@ export default {
357
469
  if (!ready) return;
358
470
  this.panel = panel;
359
471
  },
472
+ chooseVoiceModel(item) { return this.patchVoice("model", voiceValueFromMenu(item.id)); },
473
+ chooseVoiceLanguage(item) { return this.patchVoice("language", voiceValueFromMenu(item.id)); },
474
+ voiceChoiceLabel(kind) {
475
+ if (!this.voice) return "—";
476
+ if (kind === "mobileMode") return this.tVoice(`settings.mode.${this.voice.config?.mobileMode || "continuous"}`);
477
+ if (kind === "device") return this.tVoice(`settings.device.${this.voice.config?.device || "auto"}`);
478
+ return this.tVoice(`settings.mode.${this.voice.config?.streaming ? "streaming" : "turns"}`);
479
+ },
360
480
  async pick(item, panel = this.panel) {
481
+ if (item.disabled) return;
361
482
  if (item.id === this.sheetCurrentFor(panel)) {
362
483
  this.panel = "";
363
484
  return;
@@ -366,12 +487,18 @@ export default {
366
487
  if (panel === "model") await this.chooseModel(item.model);
367
488
  if (panel === "voiceModel") await this.patchVoice("model", voiceValueFromMenu(item.id));
368
489
  if (panel === "voiceLang") await this.patchVoice("language", voiceValueFromMenu(item.id));
490
+ if (panel === "voiceMobileMode") await this.patchVoice("mobileMode", item.id);
491
+ if (panel === "voiceDevice") await this.patchVoice("device", item.id);
492
+ if (panel === "voiceReading") this.chooseReading(item.id);
369
493
  if (panel === "search") await this.chooseSearch(item.id);
370
494
  },
371
495
  sheetCurrentFor(panel) {
372
496
  if (panel === "model") return this.modelCurrent;
373
497
  if (panel === "voiceModel") return voiceMenuValue(this.voice?.config?.model);
374
498
  if (panel === "voiceLang") return voiceMenuValue(this.voice?.config?.language);
499
+ if (panel === "voiceMobileMode") return this.voice?.config?.mobileMode || "continuous";
500
+ if (panel === "voiceDevice") return this.voice?.config?.device || "auto";
501
+ if (panel === "voiceReading") return this.voice?.config?.streaming ? "streaming" : "turns";
375
502
  if (panel === "search") return searchMenuValue(this.search);
376
503
  return "";
377
504
  },
@@ -387,7 +514,10 @@ export default {
387
514
  this.voice = remembered.voice ?? this.voice;
388
515
  this.search = remembered.search ?? this.search;
389
516
  this.conversation = remembered.conversation ?? this.conversation;
390
- if (this.models && this.voice && this.search && this.conversation) return;
517
+ if (this.models && this.voice && this.search && this.conversation) {
518
+ void this.loadReading();
519
+ return;
520
+ }
391
521
  }
392
522
  this.panel = "";
393
523
  this.loading = true;
@@ -410,6 +540,21 @@ export default {
410
540
  if (conversation.status === "fulfilled") this.conversation = conversation.value;
411
541
  else this.conversationError = conversation.reason instanceof Error ? conversation.reason.message : String(conversation.reason);
412
542
  this.loading = false;
543
+ void this.loadReading();
544
+ },
545
+ async loadReading() {
546
+ try {
547
+ this.reading = await this.settings.readingMode();
548
+ } catch {
549
+ this.reading = { streaming: false, allowed: false, gpu: null };
550
+ }
551
+ },
552
+ chooseReading(value) {
553
+ if (value === "streaming" && !this.reading?.allowed) {
554
+ this.voiceError = this.tVoice("error.voice_streaming_time_slice");
555
+ return;
556
+ }
557
+ void this.patchVoice("streaming", value === "streaming");
413
558
  },
414
559
  async chooseModel(model) {
415
560
  const selection = { provider: model.provider, model: model.id };
@@ -431,8 +576,10 @@ export default {
431
576
  this.voiceError = "";
432
577
  try {
433
578
  this.voice = await this.$refs.voicePreferences.patch({ [key]: value });
434
- } catch {
435
- this.voiceError = this.tVoice("settings.saveFailed");
579
+ } catch (err) {
580
+ const code = err?.code;
581
+ const text = code ? this.tVoice(`error.${code}`) : "";
582
+ this.voiceError = text && text !== `error.${code}` ? text : this.tVoice("settings.saveFailed");
436
583
  } finally {
437
584
  this.pending = "";
438
585
  }
@@ -544,22 +691,45 @@ export default {
544
691
  gap: 8px;
545
692
  }
546
693
 
547
- .lares-agent__group h2 {
694
+ .lares-agent__group h2,
695
+ .lares-agent__group h3 {
548
696
  margin: 0;
549
- font-size: 18px;
550
697
  font-weight: 500;
698
+ color: var(--q-ink-1);
699
+ }
700
+
701
+ .lares-agent__group h2 {
702
+ font-size: 18px;
551
703
  line-height: 24px;
552
704
  }
553
705
 
706
+ .lares-agent__group h3 {
707
+ margin-top: 8px;
708
+ color: var(--q-ink-3);
709
+ font-size: 14px;
710
+ line-height: 18px;
711
+ }
712
+
554
713
  .lares-agent__list {
555
714
  display: flex;
556
715
  flex-direction: column;
557
716
  gap: 12px;
558
717
  }
559
718
 
719
+ .lares-agent__field {
720
+ display: flex;
721
+ flex-direction: column;
722
+ gap: 8px;
723
+ min-width: 0;
724
+ color: var(--q-ink-1);
725
+ font-size: 15px;
726
+ line-height: 22px;
727
+ }
728
+
560
729
  .lares-agent__hint {
561
730
  margin: 0;
562
731
  font-size: 15px;
732
+ line-height: 22px;
563
733
  color: var(--q-ink-3);
564
734
  }
565
735
 
@@ -27,7 +27,7 @@
27
27
  class="lares-select__option"
28
28
  role="option"
29
29
  :aria-selected="item.id === value"
30
- :disabled="disabled"
30
+ :disabled="disabled || item.disabled"
31
31
  @click="choose(item)"
32
32
  >
33
33
  <span>{{ item.label }}</span>
@@ -75,6 +75,7 @@ export default {
75
75
  },
76
76
  methods: {
77
77
  choose(item) {
78
+ if (this.disabled || item.disabled) return;
78
79
  this.open = false;
79
80
  this.$emit("select", item);
80
81
  },
@@ -177,6 +178,11 @@ export default {
177
178
  background: var(--q-background-hover);
178
179
  }
179
180
 
181
+ .lares-select__option:disabled {
182
+ cursor: default;
183
+ opacity: 0.45;
184
+ }
185
+
180
186
  .lares-select__option[aria-selected="true"] span {
181
187
  color: var(--q-blue-default);
182
188
  }
@@ -19,6 +19,9 @@
19
19
  <div class="lares-sheet__body">
20
20
  <slot />
21
21
  </div>
22
+ <footer v-if="$slots.footer" class="lares-sheet__foot">
23
+ <slot name="footer" />
24
+ </footer>
22
25
  </div>
23
26
  </div>
24
27
  </Teleport>
@@ -84,6 +87,11 @@ export default {
84
87
  transform: translateY(0);
85
88
  }
86
89
 
90
+ .lares-sheet:has(.lares-sheet__foot) .lares-sheet__panel {
91
+ height: 45%;
92
+ overflow: hidden;
93
+ }
94
+
87
95
  .lares-sheet__head {
88
96
  display: flex;
89
97
  align-items: center;
@@ -129,6 +137,17 @@ export default {
129
137
  -webkit-overflow-scrolling: touch;
130
138
  }
131
139
 
140
+ .lares-sheet:has(.lares-sheet__foot) .lares-sheet__body {
141
+ flex: 1 1 auto;
142
+ padding-bottom: 8px;
143
+ }
144
+
145
+ .lares-sheet__foot {
146
+ flex-shrink: 0;
147
+ padding: 0 0 env(safe-area-inset-bottom, 0px);
148
+ background: var(--q-background-1);
149
+ }
150
+
132
151
  @media (prefers-reduced-motion: reduce) {
133
152
  .lares-sheet__backdrop,
134
153
  .lares-sheet__panel {