@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.
@@ -1,32 +1,48 @@
1
1
  <template>
2
2
  <div v-if="view.value" class="lares-voice-preferences">
3
- <label>{{ t('preferences.dictationStyle') }}
4
- <select :value="view.value.config.dictationPreset" :disabled="guardView.selecting || Boolean(view.pending.dictationPreset)" @change="selectPreset($event)">
5
- <option v-for="id in presets" :key="id" :value="id">{{ t(`dictation.${id}`) }}</option>
6
- </select>
3
+ <label v-if="device === 'desktop'" class="lares-voice-preferences__field">
4
+ <span>{{ t('preferences.dictationStyle') }}</span>
5
+ <LaresSelect :value="view.value.config.dictationPreset" :sections="presetSections" :disabled="guardView.selecting || Boolean(view.pending.dictationPreset)" @select="choosePreset" />
7
6
  </label>
7
+ <LaresSettingRow
8
+ v-else
9
+ :label="t('preferences.dictationStyle')"
10
+ :value="t(`dictation.${view.value.config.dictationPreset}`)"
11
+ chevron
12
+ boxed
13
+ :disabled="guardView.selecting || Boolean(view.pending.dictationPreset)"
14
+ @click="sheet = 'preset'"
15
+ />
8
16
  <p v-if="view.errors.dictationPreset" role="alert">{{ t('settings.saveFailed') }}</p>
9
17
  <p v-if="view.value.config.dictationPreset === 'raw'">{{ t('dictation.noLlm') }}</p>
10
18
  <div v-else class="lares-voice-preferences__editor">
11
- <label>{{ t('dictation.model') }}
12
- <select :value="profile.model" :disabled="profilePending" @change="preferences.edit(view.value.config.dictationPreset, { model: $event.target.value })">
13
- <option value="">{{ t('settings.model.none') }}</option>
14
- <option v-for="item in models" :key="item.id" :value="item.id" :disabled="item.disabled">{{ item.label }}</option>
15
- </select>
19
+ <label v-if="device === 'desktop'" class="lares-voice-preferences__field">
20
+ <span>{{ t('dictation.model') }}</span>
21
+ <LaresSelect :value="profile.model" :sections="modelSections" :disabled="profilePending" @select="chooseModel" />
16
22
  </label>
17
- <label>{{ t('dictation.instructions') }}
23
+ <LaresSettingRow
24
+ v-else
25
+ :label="t('dictation.model')"
26
+ :value="modelLabel"
27
+ chevron
28
+ boxed
29
+ :disabled="profilePending"
30
+ @click="sheet = 'model'"
31
+ />
32
+ <label class="lares-voice-preferences__field">
33
+ <span>{{ t('dictation.instructions') }}</span>
18
34
  <textarea ref="instructions" :value="profile.instructions" :disabled="profilePending" rows="6" maxlength="8000" @input="preferences.edit(view.value.config.dictationPreset, { instructions: $event.target.value })" />
19
35
  </label>
20
36
  <div class="lares-voice-preferences__actions">
21
37
  <button type="button" :disabled="profilePending" @click="preferences.restore(view.value.config.dictationPreset)">{{ t('dictation.reset') }}</button>
22
38
  <template v-if="preferences.dirty(view.value.config.dictationPreset)">
23
39
  <button type="button" :disabled="profilePending" @click="preferences.discard([view.value.config.dictationPreset])">{{ t('preferences.cancel') }}</button>
24
- <button type="button" :disabled="profilePending" @click="saveProfile">{{ t(profilePending ? 'preferences.saving' : 'preferences.save') }}</button>
40
+ <button type="button" class="is-primary" :disabled="profilePending" @click="saveProfile">{{ t(profilePending ? 'preferences.saving' : 'preferences.save') }}</button>
25
41
  </template>
26
42
  </div>
27
43
  <p v-if="view.errors[`profile:${view.value.config.dictationPreset}`]" role="alert">{{ t('settings.saveFailed') }}</p>
28
44
  </div>
29
- <TtsSettings :preferences="preferences" :view="view" :t="t" :fetcher="ttsFetch" />
45
+ <TtsSettings :preferences="preferences" :view="view" :t="t" :fetcher="ttsFetch" :device="device" />
30
46
  <section v-if="device === 'desktop'" class="lares-voice-preferences__shortcuts">
31
47
  <h3>{{ t('preferences.shortcuts') }}</h3>
32
48
  <p>{{ t('preferences.shortcutScope') }}</p>
@@ -50,9 +66,19 @@
50
66
  <p v-if="guardView.error" role="alert">{{ t('settings.saveFailed') }}</p>
51
67
  <button type="button" :disabled="guardView.saving" @click="guard.answer('continue')">{{ t('preferences.continue') }}</button>
52
68
  <button type="button" :disabled="guardView.saving" @click="guard.answer('discard')">{{ t('preferences.discard') }}</button>
53
- <button type="button" :disabled="guardView.saving" @click="guard.answer('save')">{{ t(guardView.saving ? 'preferences.saving' : 'preferences.save') }}</button>
69
+ <button type="button" class="is-primary" :disabled="guardView.saving" @click="guard.answer('save')">{{ t(guardView.saving ? 'preferences.saving' : 'preferences.save') }}</button>
54
70
  </div>
55
71
  </LaresSheet>
72
+ <LaresSheet v-if="device !== 'desktop'" :open="Boolean(sheet)" :title="sheetTitle" @close="sheet = ''">
73
+ <LaresSettingRow
74
+ v-for="item in sheetItems"
75
+ :key="item.id || 'none'"
76
+ :label="item.label"
77
+ :checked="item.id === sheetCurrent"
78
+ :disabled="item.disabled || (sheet === 'preset' ? guardView.selecting : profilePending)"
79
+ @click="chooseSheet(item)"
80
+ />
81
+ </LaresSheet>
56
82
  </div>
57
83
  <p v-else role="status">{{ t(view.loadError ? 'settings.loadFailed' : 'settings.loading') }}</p>
58
84
  </template>
@@ -63,10 +89,12 @@ import { createVoicePreferences, createPreferencesGuard } from "@olares/lares-co
63
89
  import { createVoiceShortcutRecorder, formatVoiceShortcut } from "@olares/lares-core/voice/shortcuts";
64
90
  import { DICTATION_PRESETS } from "@olares/lares-core/voice/dictation";
65
91
  import LaresSheet from "./Sheet.vue";
92
+ import LaresSettingRow from "./SettingRow.vue";
93
+ import LaresSelect from "./Select.vue";
66
94
  import TtsSettings from "./TtsSettings.vue";
67
95
 
68
96
  export default {
69
- components: { LaresSheet, TtsSettings },
97
+ components: { LaresSheet, LaresSettingRow, LaresSelect, TtsSettings },
70
98
  props: { ttsFetch: { type: Function, required: true }, settings: { type: Object, required: true }, snapshot: { type: Object, default: null }, device: { type: String, default: "mobile" }, t: { type: Function, required: true }, bindBeforeLeave: { type: Function, default: undefined } },
71
99
  expose: ["beforeLeave", "reload", "patch"],
72
100
  emits: ["change"],
@@ -76,10 +104,16 @@ export default {
76
104
  if (this.snapshot) preferences.accept(toRaw(this.snapshot));
77
105
  const guard = markRaw(createPreferencesGuard(preferences));
78
106
  const recorder = markRaw(createVoiceShortcutRecorder({ current: () => preferences.getSnapshot().value.config.dictationShortcuts, save: (id, key) => preferences.saveShortcut(id, key) }));
79
- return { preferences, guard, recorder, view: preferences.getSnapshot(), guardView: guard.getSnapshot(), shortcut: recorder.getSnapshot(), bindings: markRaw({}), presets: DICTATION_PRESETS, shortcutIds: ["default", ...DICTATION_PRESETS] };
107
+ return { preferences, guard, recorder, view: preferences.getSnapshot(), guardView: guard.getSnapshot(), shortcut: recorder.getSnapshot(), bindings: markRaw({}), presets: DICTATION_PRESETS, shortcutIds: ["default", ...DICTATION_PRESETS], sheet: "" };
80
108
  },
81
109
  computed: {
82
110
  profile() { return this.view.value && this.view.value.config.dictationPreset !== "raw" ? this.preferences.profile(this.view.value.config.dictationPreset) : null; },
111
+ presetSections() { return [{ items: this.presets.map(id => ({ id, label: this.t(`dictation.${id}`) })) }]; },
112
+ modelSections() { return [{ items: [{ id: "", label: this.t("settings.model.none") }, ...this.models.map(item => ({ id: item.id, label: item.label, disabled: item.disabled }))] }]; },
113
+ modelLabel() { return this.modelSections[0].items.find(item => item.id === (this.profile?.model || ""))?.label || this.t("settings.model.none"); },
114
+ sheetTitle() { return this.t(this.sheet === "model" ? "dictation.model" : "preferences.dictationStyle"); },
115
+ sheetItems() { return this.sheet === "model" ? this.modelSections[0].items : this.presetSections[0].items; },
116
+ sheetCurrent() { return this.sheet === "model" ? (this.profile?.model || "") : this.view.value?.config.dictationPreset; },
83
117
  profilePending() { return this.guardView.selecting || Boolean(this.view.pending[`profile:${this.view.value.config.dictationPreset}`]); },
84
118
  models() {
85
119
  const available = this.view.value?.chatModels || [];
@@ -117,10 +151,14 @@ export default {
117
151
  beforeLeave() { return this.guard.request(); },
118
152
  async patch(partial) { if (!this.view.value) await this.preferences.load(); return this.preferences.patch(partial); },
119
153
  reload() { return this.preferences.load(true); },
120
- async selectPreset(event) {
121
- const id = event.target.value;
122
- event.target.value = this.view.value.config.dictationPreset;
123
- await this.guard.selectPreset(id).catch(() => {});
154
+ choosePreset(item) { void this.guard.selectPreset(item.id).catch(() => {}); },
155
+ chooseModel(item) { if (!item.disabled) this.preferences.edit(this.view.value.config.dictationPreset, { model: item.id }); },
156
+ chooseSheet(item) {
157
+ if (item.disabled) return;
158
+ const sheet = this.sheet;
159
+ this.sheet = "";
160
+ if (sheet === "model") this.chooseModel(item);
161
+ else this.choosePreset(item);
124
162
  },
125
163
  async saveProfile() { try { await this.preferences.saveProfile(this.view.value.config.dictationPreset); this.$refs.instructions?.focus(); } catch { /* The shared store retains the draft and error. */ } },
126
164
  shortcutTitle(id) { return this.t(id === "default" ? "dictation.shortcut" : `dictation.${id}`); },
@@ -135,19 +173,29 @@ export default {
135
173
  </script>
136
174
 
137
175
  <style scoped>
138
- .lares-voice-preferences { display: flex; flex-direction: column; gap: 12px; min-width: 0; }
139
- .lares-voice-preferences h3 { margin: 8px 0 0; font-size: 15px; font-weight: 500; }
140
- .lares-voice-preferences p { margin: 0; font-size: 13px; color: var(--q-ink-3); }
176
+ .lares-voice-preferences { display: flex; flex-direction: column; gap: 12px; min-width: 0; color: var(--q-ink-1); font-size: 15px; line-height: 22px; }
177
+ .lares-voice-preferences h3 { margin: 4px 0 0; color: var(--q-ink-3); font-size: 14px; font-weight: 500; line-height: 18px; }
178
+ .lares-voice-preferences p { margin: 0; color: var(--q-ink-3); font-size: 15px; line-height: 22px; }
141
179
  .lares-voice-preferences [role="alert"] { color: var(--q-orange-default); }
142
- .lares-voice-preferences__row { display: flex; gap: 8px; align-items: center; justify-content: space-between; padding: 10px 0; border-bottom: 1px solid var(--q-separator); }
143
- .lares-voice-preferences__row > span { flex: 1; min-width: 0; }
180
+ .lares-voice-preferences__field { display: flex; flex-direction: column; gap: 8px; min-width: 0; }
181
+ .lares-voice-preferences__field > span { color: var(--q-ink-2); }
182
+ .lares-voice-preferences__row { display: flex; gap: 12px; align-items: center; justify-content: space-between; min-height: 56px; padding: 12px 0; border-bottom: 1px solid var(--q-separator); }
183
+ .lares-voice-preferences__row > span { flex: 1; min-width: 0; font-size: 16px; line-height: 22px; }
144
184
  .lares-voice-preferences__actions { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
145
- .lares-voice-preferences button, .lares-voice-preferences select { font: inherit; color: inherit; background: var(--q-background-2, transparent); border: 1px solid var(--q-separator); border-radius: 8px; padding: 6px 8px; cursor: pointer; max-width: 100%; }
146
- .lares-voice-preferences button:disabled { opacity: .45; cursor: default; }
147
- .lares-voice-preferences button[data-recording="true"] { outline: 2px solid var(--q-blue-default, #3978f6); }
148
- .lares-voice-preferences button:focus-visible, .lares-voice-preferences textarea:focus-visible { outline: 2px solid var(--q-blue-default, #3978f6); outline-offset: 2px; }
149
- .lares-voice-preferences__editor, .lares-voice-preferences label { display: flex; flex-direction: column; gap: 10px; }
150
- .lares-voice-preferences textarea { box-sizing: border-box; width: 100%; min-height: 144px; padding: 10px; resize: vertical; color: inherit; background: transparent; border: 1px solid var(--q-separator); border-radius: 8px; font: inherit; line-height: 1.5; }
151
- .lares-voice-preferences__shortcuts, .lares-voice-preferences__question { display: flex; flex-direction: column; gap: 12px; }
152
- .lares-voice-preferences__question { padding: 12px 20px; }
185
+ .lares-voice-preferences__actions button,
186
+ .lares-voice-preferences__question button,
187
+ .lares-voice-preferences__shortcuts button { min-height: 36px; border: 1px solid var(--q-separator); border-radius: 12px; padding: 0 14px; background: transparent; color: inherit; font: inherit; font-size: 15px; line-height: 20px; cursor: pointer; }
188
+ .lares-voice-preferences__actions button:disabled,
189
+ .lares-voice-preferences__question button:disabled,
190
+ .lares-voice-preferences__shortcuts button:disabled { opacity: .45; cursor: default; }
191
+ .lares-voice-preferences button.is-primary { border: 0; background: var(--q-blue-alpha); color: var(--q-blue-default); font-weight: 500; }
192
+ .lares-voice-preferences button[data-recording="true"] { outline: 2px solid var(--q-blue-default); }
193
+ .lares-voice-preferences__actions button:focus-visible,
194
+ .lares-voice-preferences__question button:focus-visible,
195
+ .lares-voice-preferences__shortcuts button:focus-visible,
196
+ .lares-voice-preferences textarea:focus-visible { outline: 2px solid var(--q-blue-default); outline-offset: 2px; }
197
+ .lares-voice-preferences__editor, .lares-voice-preferences__shortcuts, .lares-voice-preferences__question { display: flex; flex-direction: column; gap: 12px; }
198
+ .lares-voice-preferences textarea { box-sizing: border-box; width: 100%; min-height: 144px; padding: 12px 16px; resize: vertical; color: inherit; background: transparent; border: 1px solid var(--q-separator); border-radius: 16px; font: inherit; font-size: 16px; line-height: 1.5; }
199
+ .lares-voice-preferences__question { padding: 4px 20px 12px; }
200
+ .lares-voice-preferences__question button { width: 100%; }
153
201
  </style>