@olares/lares-mobile 0.15.7 → 0.15.10

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.
@@ -5,22 +5,47 @@
5
5
  <figure v-for="item in group.media" :key="item.path" class="lares-turn-media">
6
6
  <figcaption class="lares-turn-media-caption">
7
7
  <span :title="item.path">{{ item.name }}</span>
8
- <button type="button" class="lares-turn-open" @click="$emit('open', item.path)">{{ t("openInTab") }}</button>
8
+ <button type="button" class="lares-turn-open" @click="$emit('open', item.path)">{{ t(popup ? "viewMedia" : "openInTab") }}</button>
9
9
  </figcaption>
10
- <img
10
+ <button
11
11
  v-if="item.kind === 'image'"
12
- class="lares-turn-media-image"
13
- :src="mediaUrl(item)"
14
- :alt="item.name"
15
- @load="$emit('media')"
12
+ type="button"
13
+ class="lares-turn-media-frame"
14
+ :aria-label="t('viewMedia')"
16
15
  @click="$emit('open', item.path)"
17
- />
16
+ >
17
+ <img
18
+ class="lares-turn-media-image"
19
+ :src="mediaUrl(item)"
20
+ :alt="item.name"
21
+ @load="$emit('media')"
22
+ />
23
+ </button>
24
+ <button
25
+ v-else-if="item.kind === 'video' && popup"
26
+ type="button"
27
+ class="lares-turn-media-frame"
28
+ :aria-label="t('viewMedia')"
29
+ @click="$emit('open', item.path)"
30
+ >
31
+ <video
32
+ class="lares-turn-media-video"
33
+ :src="mediaUrl(item)"
34
+ playsinline
35
+ muted
36
+ preload="metadata"
37
+ @loadedmetadata="$emit('media')"
38
+ />
39
+ <span class="lares-turn-media-play" aria-hidden="true" />
40
+ </button>
18
41
  <video
19
42
  v-else-if="item.kind === 'video'"
20
43
  class="lares-turn-media-video"
21
44
  :src="mediaUrl(item)"
22
45
  controls
23
46
  playsinline
47
+ preload="metadata"
48
+ @loadedmetadata="$emit('media')"
24
49
  />
25
50
  <audio
26
51
  v-else-if="item.kind === 'audio'"
@@ -60,6 +85,7 @@ export default {
60
85
  props: {
61
86
  group: { type: Object, required: true },
62
87
  mediaUrl: { type: Function, required: true },
88
+ popup: { type: Boolean, default: false },
63
89
  t: { type: Function, required: true },
64
90
  },
65
91
  emits: ["open", "media"],
@@ -90,8 +116,9 @@ export default {
90
116
 
91
117
  .lares-turn-media {
92
118
  display: flex;
119
+ box-sizing: border-box;
93
120
  width: fit-content;
94
- max-width: 100%;
121
+ max-width: min(100%, 660px);
95
122
  flex-direction: column;
96
123
  gap: 8px;
97
124
  margin: 0;
@@ -136,10 +163,52 @@ export default {
136
163
  background: var(--q-background-hover);
137
164
  }
138
165
 
166
+ .lares-turn-media-frame {
167
+ position: relative;
168
+ display: block;
169
+ width: fit-content;
170
+ max-width: 100%;
171
+ margin: 0;
172
+ padding: 0;
173
+ border: 0;
174
+ background: transparent;
175
+ cursor: pointer;
176
+ }
177
+
178
+ .lares-turn-media-frame:active {
179
+ opacity: 0.86;
180
+ }
181
+
182
+ .lares-turn-media-frame img,
183
+ .lares-turn-media-frame video {
184
+ pointer-events: none;
185
+ }
186
+
187
+ .lares-turn-media-play {
188
+ position: absolute;
189
+ top: 50%;
190
+ left: 50%;
191
+ width: 52px;
192
+ height: 52px;
193
+ border-radius: 999px;
194
+ background: rgb(0 0 0 / 55%);
195
+ transform: translate(-50%, -50%);
196
+ }
197
+
198
+ .lares-turn-media-play::before {
199
+ content: "";
200
+ position: absolute;
201
+ top: 16px;
202
+ left: 20px;
203
+ border-width: 10px 0 10px 16px;
204
+ border-style: solid;
205
+ border-color: transparent transparent transparent #fff;
206
+ }
207
+
139
208
  .lares-turn-media-image,
140
209
  .lares-turn-media-video {
141
210
  display: block;
142
- max-width: min(100%, 640px);
211
+ max-width: 100%;
143
212
  max-height: 420px;
144
213
  border-radius: 10px;
145
214
  object-fit: contain;
@@ -147,11 +216,14 @@ export default {
147
216
 
148
217
  .lares-turn-media-audio {
149
218
  display: block;
150
- width: min(520px, 100%);
219
+ width: 520px;
220
+ max-width: 100%;
151
221
  }
152
222
 
153
223
  .lares-turn-media-model3d {
154
- width: min(520px, 100%);
224
+ box-sizing: border-box;
225
+ width: 520px;
226
+ max-width: 100%;
155
227
  padding: 16px;
156
228
  color: var(--q-ink-3);
157
229
  font-size: 15px;
@@ -9,6 +9,17 @@
9
9
  <aside class="lares-history__sheet" role="dialog" :aria-label="t('bar.history')">
10
10
  <header class="lares-history__head">
11
11
  <h2 class="lares-history__title">{{ t("bar.history") }}</h2>
12
+ <button
13
+ type="button"
14
+ class="lares-history__new"
15
+ :aria-label="t('bar.newAria')"
16
+ @click="$emit('create', null)"
17
+ >
18
+ <svg viewBox="0 0 24 24" aria-hidden="true">
19
+ <path d="M5 4.2h14a2.2 2.2 0 0 1 2.2 2.2v8.2a2.2 2.2 0 0 1-2.2 2.2h-5.2L12 19.8l-1.8-3H5a2.2 2.2 0 0 1-2.2-2.2V6.4A2.2 2.2 0 0 1 5 4.2z" />
20
+ <path d="M12 7.6v5.2M9.4 10.2h5.2" />
21
+ </svg>
22
+ </button>
12
23
  </header>
13
24
  <header class="lares-history__toolbar">
14
25
  <span v-if="!searchOpen" class="lares-history__section-label">{{ sectionTitle }}</span>
@@ -517,7 +528,7 @@ export default {
517
528
  align-items: center;
518
529
  flex-shrink: 0;
519
530
  height: 56px;
520
- padding: 0 20px;
531
+ padding: 0 12px 0 20px;
521
532
  }
522
533
 
523
534
  .lares-history__title {
@@ -527,6 +538,34 @@ export default {
527
538
  line-height: 26px;
528
539
  }
529
540
 
541
+ .lares-history__new {
542
+ display: inline-flex;
543
+ align-items: center;
544
+ justify-content: center;
545
+ width: 36px;
546
+ height: 36px;
547
+ margin-left: auto;
548
+ border: 0;
549
+ border-radius: 999px;
550
+ padding: 0;
551
+ background: var(--q-background-3);
552
+ color: var(--q-ink-2);
553
+ }
554
+
555
+ .lares-history__new svg {
556
+ width: 22px;
557
+ height: 22px;
558
+ fill: none;
559
+ stroke: currentColor;
560
+ stroke-width: 1.75;
561
+ stroke-linecap: round;
562
+ stroke-linejoin: round;
563
+ }
564
+
565
+ .lares-history__new:active {
566
+ background: var(--q-btn-bg-pressed);
567
+ }
568
+
530
569
  .lares-history__toolbar {
531
570
  display: flex;
532
571
  flex-shrink: 0;
@@ -25,7 +25,7 @@ export default {
25
25
  name: "LaresQuestionCard",
26
26
  props: {
27
27
  questions: { type: Array, default: () => [] },
28
- rpcId: { type: String, default: "" },
28
+ answerable: { type: Boolean, default: false },
29
29
  busy: { type: Boolean, default: false },
30
30
  t: { type: Function, required: true },
31
31
  },
@@ -47,11 +47,11 @@ export default {
47
47
  return Array.isArray(this.current?.options) ? this.current.options : [];
48
48
  },
49
49
  disabled() {
50
- return this.busy || !this.rpcId || !this.current;
50
+ return this.busy || !this.answerable || !this.current;
51
51
  },
52
52
  hint() {
53
53
  if (this.busy) return this.t("question.busy");
54
- if (!this.rpcId) return this.t("question.waiting");
54
+ if (!this.answerable) return this.t("question.waiting");
55
55
  return "";
56
56
  },
57
57
  },
@@ -10,8 +10,25 @@
10
10
  <span class="lares-sk lares-sk--line lares-sk--w56" />
11
11
  </li>
12
12
  <li v-else-if="items.length === 0 && !running" class="lares-log__empty">{{ t("chat.empty") }}</li>
13
+ <template v-for="block in blocks" :key="block.key">
14
+ <li v-if="block.kind === 'fold'" class="lares-log__item" data-type="process">
15
+ <button
16
+ type="button"
17
+ class="lares-process"
18
+ :data-phase="block.phase"
19
+ :data-open="processOpen(block) ? 'true' : 'false'"
20
+ :disabled="!block.foldable"
21
+ :aria-expanded="processOpen(block) ? 'true' : 'false'"
22
+ @click="toggleProcess(block.key)"
23
+ >
24
+ <span class="lares-process__label">{{ processLabel(block) }}</span>
25
+ <svg v-if="block.foldable" class="lares-process__chevron" viewBox="0 0 24 24" aria-hidden="true">
26
+ <path d="M6.5 9 12 14.5 17.5 9" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" />
27
+ </svg>
28
+ </button>
29
+ </li>
13
30
  <li
14
- v-for="(row, index) in items"
31
+ v-for="{ row, index } in shown(block)"
15
32
  :key="index"
16
33
  class="lares-log__item"
17
34
  :data-type="row.type"
@@ -35,7 +52,7 @@
35
52
  <LaresQuestionCard
36
53
  v-else-if="!questionTakeover && row.type === 'tool' && row.name === 'ask_user_question' && row.status === 'running'"
37
54
  :questions="questionItems(row)"
38
- :rpc-id="question?.rpcId || ''"
55
+ :answerable="Boolean(question?.eventId)"
39
56
  :busy="questionBusy"
40
57
  :t="t"
41
58
  @pick="$emit('answer', $event, row)"
@@ -69,23 +86,30 @@
69
86
  @react="setReaction(index, $event)"
70
87
  />
71
88
  </template>
72
- <LaresDeliverables
89
+ <LaresDeliverables
73
90
  v-else-if="row.type === 'files'"
74
91
  :group="fileGroup(row.paths)"
75
92
  :media-url="mediaUrl"
93
+ :popup="popup"
76
94
  :t="t"
77
95
  @open="$emit('open', $event)"
78
96
  @media="$emit('media')"
79
97
  />
80
98
  </li>
99
+ </template>
100
+ <li v-if="running && !hideRunning" class="lares-log__running" data-chat-running="true">
101
+ <span class="lares-log__sr" role="status" aria-live="polite" aria-atomic="true">{{ t("chat.deepDiving") }}</span>
102
+ <span class="lares-log__running-text" aria-hidden="true">{{ runningText }}</span>
103
+ </li>
81
104
  </ol>
82
105
  </template>
83
106
 
84
107
  <script>
85
108
  import { partitionPreviews } from "@olares/lares-core/files/preview-groups";
86
109
  import { parseAskUserQuestions } from "@olares/lares-core/larepass/questions";
110
+ import { hidesRunningClock, processFoldLabel, transcriptBlocks } from "@olares/lares-core/larepass/process-fold";
87
111
  import { STAGE_COPY } from "@olares/lares-core/larepass/stage-copy";
88
- import { retryStatus as formatRetry, thinkSummary } from "./format.js";
112
+ import { retryStatus as formatRetry, runningLabel, thinkSummary } from "./format.js";
89
113
  import { renderMarkdown } from "./markdown.js";
90
114
  import LaresDeliverables from "./Deliverables.vue";
91
115
  import LaresQuestionCard from "./QuestionCard.vue";
@@ -98,6 +122,7 @@ export default {
98
122
  props: {
99
123
  items: { type: Array, default: () => [] },
100
124
  running: { type: Boolean, default: false },
125
+ runningSince: { type: Number, default: 0 },
101
126
  loading: { type: Boolean, default: false },
102
127
  previews: { type: Object, default: () => ({}) },
103
128
  mediaUrl: { type: Function, required: true },
@@ -105,6 +130,7 @@ export default {
105
130
  rememberScroll: { type: Function, required: true },
106
131
  sticking: { type: Function, required: true },
107
132
  sessionId: { type: String, default: "" },
133
+ popup: { type: Boolean, default: false },
108
134
  question: { type: Object, default: null },
109
135
  questionBusy: { type: Boolean, default: false },
110
136
  questionTakeover: { type: Boolean, default: false },
@@ -118,25 +144,66 @@ export default {
118
144
  logPin: 0,
119
145
  stageCopy: STAGE_COPY,
120
146
  reactions: {},
147
+ openFolds: {},
148
+ now: Date.now(),
149
+ clock: 0,
121
150
  };
122
151
  },
152
+ computed: {
153
+ runningText() {
154
+ return runningLabel(this.runningSince, this.t, this.now);
155
+ },
156
+ blocks() {
157
+ return transcriptBlocks(this.items, { running: this.running });
158
+ },
159
+ hideRunning() {
160
+ return hidesRunningClock(this.blocks);
161
+ },
162
+ },
123
163
  watch: {
124
164
  sessionId() {
125
165
  this.reactions = {};
166
+ this.openFolds = {};
126
167
  },
168
+ running: "syncClock",
169
+ runningSince: "syncClock",
127
170
  },
128
171
  mounted() {
129
172
  this.bindLog();
130
173
  this.restoreLog();
174
+ this.syncClock();
131
175
  },
132
176
  beforeUnmount() {
133
177
  this.captureLog();
134
178
  this.unbindLog();
135
179
  if (this.logPin) cancelAnimationFrame(this.logPin);
180
+ clearInterval(this.clock);
136
181
  },
137
182
  methods: {
183
+ syncClock() {
184
+ clearInterval(this.clock);
185
+ this.clock = 0;
186
+ this.now = Date.now();
187
+ if (!this.running || !this.runningSince) return;
188
+ this.clock = setInterval(() => {
189
+ this.now = Date.now();
190
+ }, 1000);
191
+ },
138
192
  renderMarkdown,
139
193
  thinkSummary,
194
+ shown(block) {
195
+ if (block.kind === "fold") return this.processOpen(block) ? block.members : [];
196
+ return [block];
197
+ },
198
+ processOpen(block) {
199
+ return !block.foldable || Boolean(this.openFolds[block.key]);
200
+ },
201
+ processLabel(block) {
202
+ return processFoldLabel(this.t, block);
203
+ },
204
+ toggleProcess(key) {
205
+ this.openFolds = { ...this.openFolds, [key]: !this.openFolds[key] };
206
+ },
140
207
  questionItems(row) {
141
208
  if (this.question?.questions?.length) return this.question.questions;
142
209
  return parseAskUserQuestions(row.argsRaw);
@@ -270,11 +337,37 @@ export default {
270
337
 
271
338
  @media (prefers-reduced-motion: reduce) {
272
339
  .lares-sk,
340
+ .lares-log__running-text,
341
+ .lares-process[data-phase="live"] .lares-process__label,
273
342
  .lares-log__item[data-pending="true"] {
274
343
  animation: none;
275
344
  }
276
345
  }
277
346
 
347
+ .lares-log__running {
348
+ margin-top: 4px;
349
+ font-size: 14px;
350
+ line-height: 20px;
351
+ }
352
+
353
+ .lares-log__running-text {
354
+ color: var(--q-blue-default);
355
+ animation: lares-running-shimmer 1.6s ease-in-out infinite;
356
+ }
357
+
358
+ @keyframes lares-running-shimmer {
359
+ 50% { opacity: 0.5; }
360
+ }
361
+
362
+ .lares-log__sr {
363
+ position: absolute;
364
+ width: 1px;
365
+ height: 1px;
366
+ overflow: hidden;
367
+ clip: rect(0 0 0 0);
368
+ white-space: nowrap;
369
+ }
370
+
278
371
  .lares-log__item {
279
372
  display: flex;
280
373
  flex-direction: column;
@@ -283,6 +376,71 @@ export default {
283
376
  max-width: 100%;
284
377
  }
285
378
 
379
+ .lares-process {
380
+ display: flex;
381
+ align-items: center;
382
+ box-sizing: border-box;
383
+ width: 100%;
384
+ min-width: 0;
385
+ min-height: 33px;
386
+ border: 0;
387
+ border-bottom: 0.5px solid var(--q-separator);
388
+ padding: 0 0 8px;
389
+ background: transparent;
390
+ color: var(--q-ink-3);
391
+ font: inherit;
392
+ text-align: left;
393
+ cursor: pointer;
394
+ }
395
+
396
+ .lares-process:disabled {
397
+ cursor: default;
398
+ }
399
+
400
+ .lares-process[data-phase="live"],
401
+ .lares-process[data-phase="summary"] {
402
+ min-height: 0;
403
+ border-bottom: 0;
404
+ padding: 0;
405
+ }
406
+
407
+ .lares-process[data-phase="live"] .lares-process__label {
408
+ animation: lares-running-shimmer 1.6s ease-in-out infinite;
409
+ }
410
+
411
+ @media (hover: hover) {
412
+ .lares-process:hover:not(:disabled) {
413
+ color: var(--q-ink-2);
414
+ }
415
+ }
416
+
417
+ .lares-process__label {
418
+ min-width: 0;
419
+ overflow: hidden;
420
+ font-size: 14px;
421
+ line-height: 24px;
422
+ text-overflow: ellipsis;
423
+ white-space: nowrap;
424
+ }
425
+
426
+ .lares-process__chevron {
427
+ flex: none;
428
+ width: 14px;
429
+ height: 14px;
430
+ margin-left: 4px;
431
+ transition: transform 0.1s;
432
+ }
433
+
434
+ .lares-process[data-open="true"] .lares-process__chevron {
435
+ transform: rotate(180deg);
436
+ }
437
+
438
+ @media (prefers-reduced-motion: reduce) {
439
+ .lares-process__chevron {
440
+ transition: none;
441
+ }
442
+ }
443
+
286
444
  .lares-log__item[data-pending="true"] {
287
445
  animation: lares-msg-in var(--lares-duration-normal, 220ms) var(--lares-ease-out, cubic-bezier(0.32, 0.72, 0, 1));
288
446
  }
@@ -87,6 +87,17 @@ export function metricsLine(metrics, t, now = Date.now()) {
87
87
  return parts.join(" · ");
88
88
  }
89
89
 
90
+ /** The open turn's live clock; dsh never shows less than one second. */
91
+ export function runningLabel(since, t, now = Date.now()) {
92
+ if (!since) return t("chat.deepDiving");
93
+ const { minutes, seconds } = durationParts(Math.max(1000, now - since));
94
+ return t("chat.deepDivingFor", {
95
+ duration: minutes > 0
96
+ ? t("duration.minutes", { minutes, seconds })
97
+ : t("duration.seconds", { seconds }),
98
+ });
99
+ }
100
+
90
101
  export function retryStatus(row) {
91
102
  const copy = STAGE_COPY.retry;
92
103
  const label = row.retryState === "started"
@@ -0,0 +1,41 @@
1
+ const SILENT_WAV = "data:audio/wav;base64,UklGRiQAAABXQVZFZm10IBAAAAABAAEAQB8AAEAfAAABAAgAZGF0YQAAAAA=";
2
+
3
+ /**
4
+ * One element, unlocked inside the gesture that asks the question (a voice
5
+ * press or a send tap), then reused for the answer. iOS only lets an element
6
+ * play later if it first played inside a gesture.
7
+ */
8
+ export function createAnswerPlayer() {
9
+ const audio = typeof Audio === "function" ? new Audio() : null;
10
+ let url = "";
11
+ const release = () => {
12
+ if (!url) return;
13
+ URL.revokeObjectURL(url);
14
+ url = "";
15
+ };
16
+ return {
17
+ unlock() {
18
+ if (!audio || !audio.paused) return;
19
+ audio.src = SILENT_WAV;
20
+ void audio.play().catch(() => {});
21
+ },
22
+ async play(blob) {
23
+ if (!audio || !blob) return false;
24
+ release();
25
+ url = URL.createObjectURL(blob);
26
+ audio.src = url;
27
+ audio.onended = release;
28
+ try {
29
+ await audio.play();
30
+ return true;
31
+ } catch {
32
+ release();
33
+ return false;
34
+ }
35
+ },
36
+ stop() {
37
+ audio?.pause();
38
+ release();
39
+ },
40
+ };
41
+ }