@olares/lares-mobile 0.15.6 → 0.15.9

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.
@@ -2,11 +2,20 @@
2
2
  <div class="lares-mobile-shell">
3
3
  <LaresChatBar
4
4
  :t="t"
5
- :title="barTitle"
6
5
  :starting="state.starting"
6
+ :model-label="state.modelLabel"
7
+ :model-busy="state.modelBusy"
8
+ :effort-label="state.effortLabel"
9
+ :effort-disabled="state.effortDisabled"
10
+ :unseen="hasUnseen"
11
+ :speak="state.voiceSpeak"
12
+ :speak-busy="state.speakBusy"
7
13
  @history="$emit('history')"
8
- @create="$emit('create', state.currentWorkspaceId || null)"
14
+ @model="$emit('open-model')"
15
+ @effort="$emit('open-model')"
16
+ @speak="$emit('toggle-speak')"
9
17
  />
18
+ <p v-if="state.speakNotice" class="lares-speak-notice" role="status">{{ state.speakNotice }}</p>
10
19
  <LaresHistoryPanel
11
20
  :open="state.panel === 'history'"
12
21
  :sessions="state.sessions"
@@ -54,12 +63,6 @@
54
63
  v-if="newSessionHero"
55
64
  key="hero"
56
65
  :t="t"
57
- :workspace-label="workspaceLabel"
58
- :workspace-busy="!state.workspacesReady && !state.workspaceError"
59
- :model-label="state.modelLabel"
60
- :model-busy="state.modelBusy"
61
- @workspace="workspaceSheet = true"
62
- @model="$emit('open-model')"
63
66
  >
64
67
  <LaresComposer
65
68
  ref="composer"
@@ -67,10 +70,6 @@
67
70
  :draft="state.draft"
68
71
  :sending="state.sending"
69
72
  :can-send="state.canSend"
70
- :model-label="state.modelLabel"
71
- :model-busy="state.modelBusy"
72
- :effort-label="state.effortLabel"
73
- :effort-disabled="state.effortDisabled"
74
73
  :attach-pending="state.upload.pending > 0"
75
74
  :attach-disabled="!state.sessionId || state.historyLoading || state.starting"
76
75
  :voice-phase="state.voicePhase"
@@ -83,15 +82,15 @@
83
82
  :references-error="state.referencesError"
84
83
  :commands="state.commands"
85
84
  :commands-error="state.commandsError"
85
+ :input-mode="composerMode"
86
86
  :t="t"
87
87
  @update:draft="$emit('update-draft', $event)"
88
+ @update:input-mode="composerMode = $event"
88
89
  @send="$emit('send')"
89
- @model="$emit('open-model')"
90
- @effort="$emit('open-effort')"
91
90
  @files="$emit('files', $event)"
92
91
  @voice="$emit('voice')"
93
- @hold-start="$emit('hold-start')"
94
- @hold-end="$emit('hold-end')"
92
+ @hold-start="$emit('hold-start', $event)"
93
+ @hold-end="$emit('hold-end', $event)"
95
94
  @open-commands="$emit('open-commands')"
96
95
  @open-references="$emit('open-references', $event)"
97
96
  @pick-command="$emit('pick-command', $event)"
@@ -104,6 +103,7 @@
104
103
  ref="transcript"
105
104
  :items="state.viewItems"
106
105
  :running="state.running"
106
+ :running-since="state.runningSince"
107
107
  :loading="state.historyLoading || state.starting"
108
108
  :previews="state.previews"
109
109
  :media-url="mediaUrl"
@@ -111,6 +111,7 @@
111
111
  :remember-scroll="rememberScroll"
112
112
  :sticking="sticking"
113
113
  :session-id="state.sessionId"
114
+ popup
114
115
  :question="state.question"
115
116
  :question-busy="state.questionBusy"
116
117
  :t="t"
@@ -118,15 +119,18 @@
118
119
  @media="pinLog"
119
120
  @answer="forwardAnswer"
120
121
  />
122
+ <LaresDesktopQueue
123
+ :items="state.queue"
124
+ :running="state.running"
125
+ :busy="state.queueBusy"
126
+ :t="t"
127
+ @action="(itemId, action) => $emit('queue-action', itemId, action)"
128
+ />
121
129
  <LaresComposer
122
130
  ref="composer"
123
131
  :draft="state.draft"
124
132
  :sending="state.sending"
125
133
  :can-send="state.canSend"
126
- :model-label="state.modelLabel"
127
- :model-busy="state.modelBusy"
128
- :effort-label="state.effortLabel"
129
- :effort-disabled="state.effortDisabled"
130
134
  :attach-pending="state.upload.pending > 0"
131
135
  :attach-disabled="!state.sessionId || state.historyLoading || state.starting"
132
136
  :voice-phase="state.voicePhase"
@@ -139,15 +143,15 @@
139
143
  :references-error="state.referencesError"
140
144
  :commands="state.commands"
141
145
  :commands-error="state.commandsError"
146
+ :input-mode="composerMode"
142
147
  :t="t"
143
148
  @update:draft="$emit('update-draft', $event)"
149
+ @update:input-mode="composerMode = $event"
144
150
  @send="$emit('send')"
145
- @model="$emit('open-model')"
146
- @effort="$emit('open-effort')"
147
151
  @files="$emit('files', $event)"
148
152
  @voice="$emit('voice')"
149
- @hold-start="$emit('hold-start')"
150
- @hold-end="$emit('hold-end')"
153
+ @hold-start="$emit('hold-start', $event)"
154
+ @hold-end="$emit('hold-end', $event)"
151
155
  @open-commands="$emit('open-commands')"
152
156
  @open-references="$emit('open-references', $event)"
153
157
  @pick-command="$emit('pick-command', $event)"
@@ -157,18 +161,6 @@
157
161
  </Transition>
158
162
  </div>
159
163
 
160
- <LaresSheet :open="workspaceSheet" :title="t('desktop.workspaces')" @close="workspaceSheet = false">
161
- <p v-if="state.workspaceError" class="lares-shell__status">{{ state.workspaceError }}</p>
162
- <p v-else-if="!state.workspacesReady" class="lares-shell__hint">{{ t("agent.loading") }}</p>
163
- <p v-else-if="!state.workspaces.length" class="lares-shell__hint">{{ t("desktop.noFolders") }}</p>
164
- <LaresSettingRow
165
- v-for="workspace in state.workspaces"
166
- :key="workspace.workspaceId"
167
- :label="workspace.title"
168
- :checked="workspace.workspaceId === state.currentWorkspaceId"
169
- @click="pickWorkspace(workspace.workspaceId)"
170
- />
171
- </LaresSheet>
172
164
  <LaresSheet :open="state.modelSheet" :title="t('model.menuAria')" @close="$emit('close-model')">
173
165
  <p v-if="state.modelError" class="lares-shell__status">{{ state.modelError }}</p>
174
166
  <p v-else-if="!state.models" class="lares-shell__hint">{{ t("agent.loading") }}</p>
@@ -184,17 +176,17 @@
184
176
  @click="$emit('choose-model', item)"
185
177
  />
186
178
  </template>
187
- </LaresSheet>
188
- <LaresSheet :open="state.effortSheet" :title="t('reasoning.title')" @close="$emit('close-effort')">
189
- <p v-if="!state.effortRows.length" class="lares-shell__hint">{{ t("reasoning.default") }}</p>
190
- <LaresSettingRow
191
- v-for="row in state.effortRows"
192
- :key="row.key"
193
- :label="effortName(row)"
194
- :checked="row.id === state.effortId"
195
- :disabled="state.modelBusy"
196
- @click="$emit('choose-effort', row.id)"
197
- />
179
+ <template v-if="state.effortRows.length">
180
+ <p class="lares-shell__heading">{{ t("reasoning.title") }}</p>
181
+ <LaresSettingRow
182
+ v-for="row in state.effortRows"
183
+ :key="row.key"
184
+ :label="effortName(row)"
185
+ :checked="row.id === state.effortId"
186
+ :disabled="state.modelBusy"
187
+ @click="$emit('choose-effort', row.id)"
188
+ />
189
+ </template>
198
190
  </LaresSheet>
199
191
  <Transition name="lares-preview">
200
192
  <LaresPreview
@@ -206,6 +198,7 @@
206
198
  :error="state.preview.error"
207
199
  :media-src="state.previewMediaSrc"
208
200
  :download-href="state.previewDownloadHref"
201
+ :files-href="state.previewFilesHref"
209
202
  :href-for="previewHref"
210
203
  :t="t"
211
204
  @close="$emit('close-preview')"
@@ -221,12 +214,12 @@ import LaresChatBar from "../chat/ChatBar.vue";
221
214
  import LaresHistoryPanel from "../chat/HistoryPanel.vue";
222
215
  import LaresTranscript from "../chat/Transcript.vue";
223
216
  import LaresComposer from "../chat/Composer.vue";
217
+ import LaresDesktopQueue from "../desktop/DesktopQueue.vue";
224
218
  import LaresPreview from "../preview/Preview.vue";
225
219
  import LaresSheet from "../settings/Sheet.vue";
226
220
  import LaresSettingRow from "../settings/SettingRow.vue";
227
221
  import LaresMobileDirectorySheet from "./MobileDirectorySheet.vue";
228
222
  import LaresMobileNewSession from "./MobileNewSession.vue";
229
- import { DEFAULT_WORKSPACE_TITLE } from "@olares/lares-core/workspace/constants";
230
223
 
231
224
  export default {
232
225
  name: "LaresMobileShell",
@@ -235,6 +228,7 @@ export default {
235
228
  LaresHistoryPanel,
236
229
  LaresTranscript,
237
230
  LaresComposer,
231
+ LaresDesktopQueue,
238
232
  LaresPreview,
239
233
  LaresSheet,
240
234
  LaresSettingRow,
@@ -253,18 +247,18 @@ export default {
253
247
  effortName: { type: Function, required: true },
254
248
  },
255
249
  emits: [
256
- "history", "create", "close-history", "pick-session", "retry", "open-file",
257
- "answer", "update-draft", "send", "open-model", "open-effort", "files",
250
+ "history", "create", "toggle-speak", "close-history", "pick-session", "retry", "open-file",
251
+ "answer", "update-draft", "send", "queue-action", "open-model", "open-effort", "files",
258
252
  "voice", "hold-start", "hold-end", "close-model", "choose-model",
259
253
  "close-effort", "choose-effort", "close-preview",
260
254
  "open-commands", "open-references", "pick-command", "remove-image",
261
255
  "refresh-workspaces", "browse-workspaces", "browse-directory",
262
256
  "close-directory", "choose-directory", "create-folder", "toggle-hidden",
263
257
  "rename-session", "archive-session",
264
- "move-session", "pick-workspace",
258
+ "move-session",
265
259
  ],
266
260
  data() {
267
- return { workspaceSheet: false };
261
+ return { composerMode: "text" };
268
262
  },
269
263
  computed: {
270
264
  newSessionHero() {
@@ -276,25 +270,12 @@ export default {
276
270
  && !this.state.starting
277
271
  );
278
272
  },
279
- barTitle() {
280
- return this.newSessionHero ? this.t("desktop.newSession") : this.state.sessionTitle;
281
- },
282
- workspaceLabel() {
283
- if (!this.state.workspacesReady) return this.t("agent.loading");
284
- const current = this.state.workspaces.find(
285
- (row) => row.workspaceId === this.state.currentWorkspaceId,
286
- );
287
- if (current?.title) return current.title;
288
- if (this.state.workspaces[0]?.title) return this.state.workspaces[0].title;
289
- return DEFAULT_WORKSPACE_TITLE;
273
+ hasUnseen() {
274
+ const unseen = this.state.unseen;
275
+ return Boolean(unseen && Object.keys(unseen).length);
290
276
  },
291
277
  },
292
278
  methods: {
293
- pickWorkspace(workspaceId) {
294
- this.workspaceSheet = false;
295
- if (!workspaceId || workspaceId === this.state.currentWorkspaceId) return;
296
- this.$emit("pick-workspace", workspaceId);
297
- },
298
279
  forwardAnswer(label, row) {
299
280
  this.$emit("answer", label, row);
300
281
  },
@@ -324,7 +305,7 @@ export default {
324
305
  }
325
306
 
326
307
  .lares-shell__conversation[data-hero="true"] {
327
- justify-content: center;
308
+ justify-content: flex-start;
328
309
  }
329
310
 
330
311
  .lares-shell__chat {
@@ -346,6 +327,13 @@ export default {
346
327
  color: var(--q-orange-default);
347
328
  }
348
329
 
330
+ .lares-speak-notice {
331
+ margin: 0 16px 8px;
332
+ color: var(--q-orange-default);
333
+ font-size: 14px;
334
+ line-height: 20px;
335
+ }
336
+
349
337
  .lares-shell__retry {
350
338
  align-self: flex-start;
351
339
  margin: 0 20px;
@@ -13,14 +13,21 @@
13
13
  </svg>
14
14
  </button>
15
15
  <h1 class="lares-preview__name" :title="path">{{ name }}</h1>
16
- <a
17
- v-if="downloadHref"
18
- class="lares-preview__download"
19
- :href="downloadHref"
20
- download
21
- >
22
- {{ t("download") }}
23
- </a>
16
+ <div v-if="filesHref || downloadHref" class="lares-preview__actions">
17
+ <a
18
+ v-if="filesHref"
19
+ class="lares-preview__action"
20
+ :href="filesHref"
21
+ target="_blank"
22
+ rel="noopener noreferrer"
23
+ >{{ t("openInFiles") }}</a>
24
+ <a
25
+ v-if="downloadHref"
26
+ class="lares-preview__action"
27
+ :href="downloadHref"
28
+ download
29
+ >{{ t("download") }}</a>
30
+ </div>
24
31
  <span v-else class="lares-preview__side-spacer" aria-hidden="true" />
25
32
  </header>
26
33
  <LaresPreviewBody
@@ -54,6 +61,7 @@ export default {
54
61
  error: { type: String, default: "" },
55
62
  mediaSrc: { type: String, default: "" },
56
63
  downloadHref: { type: String, default: "" },
64
+ filesHref: { type: String, default: "" },
57
65
  hrefFor: { type: Function, default: null },
58
66
  t: { type: Function, required: true },
59
67
  },
@@ -83,10 +91,11 @@ export default {
83
91
 
84
92
  .lares-preview__bar {
85
93
  position: relative;
86
- display: flex;
94
+ display: grid;
95
+ grid-template-columns: 40px minmax(0, 1fr) auto;
96
+ column-gap: 8px;
87
97
  flex-shrink: 0;
88
98
  align-items: center;
89
- justify-content: space-between;
90
99
  min-height: 56px;
91
100
  padding: 6px 8px;
92
101
  }
@@ -124,9 +133,8 @@ export default {
124
133
  }
125
134
 
126
135
  .lares-preview__name {
127
- position: absolute;
128
- left: 50%;
129
- max-width: calc(100% - 152px);
136
+ min-width: 0;
137
+ max-width: 100%;
130
138
  margin: 0;
131
139
  overflow: hidden;
132
140
  font-size: 18px;
@@ -135,11 +143,17 @@ export default {
135
143
  text-align: center;
136
144
  text-overflow: ellipsis;
137
145
  white-space: nowrap;
138
- transform: translateX(-50%);
139
146
  pointer-events: none;
140
147
  }
141
148
 
142
- .lares-preview__download {
149
+ .lares-preview__actions {
150
+ display: flex;
151
+ flex-shrink: 0;
152
+ align-items: center;
153
+ gap: 6px;
154
+ }
155
+
156
+ .lares-preview__action {
143
157
  display: inline-flex;
144
158
  min-width: 40px;
145
159
  height: 32px;
@@ -158,7 +172,7 @@ export default {
158
172
  text-decoration: none;
159
173
  }
160
174
 
161
- .lares-preview__download:active {
175
+ .lares-preview__action:active {
162
176
  background: var(--q-btn-bg-pressed);
163
177
  }
164
178
  </style>
@@ -154,22 +154,25 @@ export default {
154
154
 
155
155
  .lares-preview-body__media {
156
156
  display: grid;
157
+ grid-template: minmax(0, 1fr) / minmax(0, 1fr);
157
158
  min-width: 0;
158
159
  min-height: 0;
159
160
  flex: 1;
160
161
  place-items: center;
161
- overflow: auto;
162
+ overflow: hidden;
162
163
  padding: 16px 20px;
163
164
  }
164
165
 
165
166
  .lares-preview-body__media img,
166
167
  .lares-preview-body__media video {
168
+ display: block;
167
169
  min-width: 0;
168
170
  min-height: 0;
169
171
  max-width: 100%;
170
172
  max-height: 100%;
171
173
  border-radius: 10px;
172
174
  object-fit: contain;
175
+ object-position: center;
173
176
  }
174
177
 
175
178
  .lares-preview-body__media audio {
@@ -72,7 +72,6 @@
72
72
  :value="searchMenuId"
73
73
  :sections="searchSelectSections"
74
74
  :disabled="busy || !search"
75
- :empty="tSearch('settings.default.empty')"
76
75
  @select="(item) => pick(item, 'search')"
77
76
  />
78
77
  <div v-else class="lares-agent__list">
@@ -153,6 +152,7 @@ export default {
153
152
  proxyPrefix: { type: String, default: undefined },
154
153
  request: { type: Function, default: undefined },
155
154
  env: { type: Object, default: undefined },
155
+ missing: { type: Boolean, default: false },
156
156
  },
157
157
  data() {
158
158
  const remembered = rememberedSettings();
@@ -186,6 +186,7 @@ export default {
186
186
  proxyPrefix: this.proxyPrefix,
187
187
  request: this.request,
188
188
  env: this.env,
189
+ missing: this.missing,
189
190
  };
190
191
  },
191
192
  hostKey() {
@@ -217,8 +218,8 @@ export default {
217
218
  },
218
219
  searchItems() {
219
220
  return searchSelectorItems(this.searchModels, {
220
- none: this.tSearch("settings.default.none"),
221
- empty: this.tSearch("settings.default.empty"),
221
+ routerDefault: this.tSearch("settings.default.router"),
222
+ off: this.tSearch("settings.default.off"),
222
223
  });
223
224
  },
224
225
  modelLabel() {
@@ -239,7 +240,7 @@ export default {
239
240
  return itemLabel(this.voiceLanguages, voiceMenuValue(this.voice.config?.language));
240
241
  },
241
242
  searchMenuId() {
242
- return searchMenuValue(this.search?.defaultSearchModel);
243
+ return searchMenuValue(this.search);
243
244
  },
244
245
  searchLabel() {
245
246
  if (!this.search) return "—";
@@ -291,7 +292,6 @@ export default {
291
292
  const count = this.sheetSections.reduce((sum, section) => sum + section.items.length, 0);
292
293
  if (count > 0) return "";
293
294
  if (this.panel === "model") return this.tModel("settings.empty");
294
- if (this.panel === "search") return this.tSearch("settings.default.empty");
295
295
  return "";
296
296
  },
297
297
  },
@@ -312,6 +312,15 @@ export default {
312
312
  },
313
313
  mounted() {
314
314
  this.load(false);
315
+ // LarePass has no catalog-revision stream, so returning to the panel is
316
+ // the only moment a Router change can still be picked up.
317
+ this.onVisible = () => {
318
+ if (document.visibilityState === "visible" && !this.busy) this.load(true);
319
+ };
320
+ document.addEventListener("visibilitychange", this.onVisible);
321
+ },
322
+ beforeUnmount() {
323
+ if (this.onVisible) document.removeEventListener("visibilitychange", this.onVisible);
315
324
  },
316
325
  methods: {
317
326
  tModel(key, params) {
@@ -350,7 +359,7 @@ export default {
350
359
  if (panel === "model") return this.modelCurrent;
351
360
  if (panel === "voiceModel") return voiceMenuValue(this.voice?.config?.model);
352
361
  if (panel === "voiceLang") return voiceMenuValue(this.voice?.config?.language);
353
- if (panel === "search") return searchMenuValue(this.search?.defaultSearchModel);
362
+ if (panel === "search") return searchMenuValue(this.search);
354
363
  return "";
355
364
  },
356
365
  reload() {
@@ -418,11 +427,10 @@ export default {
418
427
  },
419
428
  async chooseSearch(rawId) {
420
429
  if (!this.search || this.busy) return;
421
- const id = searchValueFromMenu(rawId);
422
430
  this.pending = "search";
423
431
  this.searchError = "";
424
432
  try {
425
- this.search = await this.settings.setSearchDefault(id);
433
+ this.search = await this.settings.setSearchDefault(searchValueFromMenu(rawId));
426
434
  } catch (err) {
427
435
  this.searchError = this.failText(this.tSearch, "settings.saveFailed", err);
428
436
  } finally {
@@ -41,10 +41,14 @@ export default {
41
41
  inset: 0;
42
42
  z-index: 40;
43
43
  pointer-events: none;
44
+ visibility: hidden;
45
+ transition: visibility 0s linear var(--lares-duration-sheet, 280ms);
44
46
  }
45
47
 
46
48
  .lares-sheet[data-open="true"] {
47
49
  pointer-events: auto;
50
+ visibility: visible;
51
+ transition: visibility 0s linear 0s;
48
52
  }
49
53
 
50
54
  .lares-sheet__backdrop {