@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.
- package/package.json +2 -2
- package/src/App.vue +255 -144
- package/src/chat/ChatBar.vue +134 -33
- package/src/chat/Composer.vue +245 -213
- package/src/chat/Deliverables.vue +83 -11
- package/src/chat/HistoryPanel.vue +40 -1
- package/src/chat/QuestionCard.vue +3 -3
- package/src/chat/Transcript.vue +162 -4
- package/src/chat/format.js +11 -0
- package/src/chat/playback.js +41 -0
- package/src/chat/voice.js +169 -39
- package/src/desktop/DesktopComposer.vue +74 -3
- package/src/desktop/DesktopPreview.vue +10 -2
- package/src/desktop/DesktopShell.vue +31 -3
- package/src/desktop/DirectoryBrowser.vue +94 -33
- package/src/desktop/ui/PcDialog.vue +23 -8
- package/src/host.js +131 -21
- package/src/index.d.ts +5 -0
- package/src/mobile/MobileNewSession.vue +6 -117
- package/src/mobile/MobileShell.vue +57 -69
- package/src/preview/Preview.vue +30 -16
- package/src/preview/PreviewBody.vue +4 -1
- package/src/settings/AgentSettings.vue +16 -8
- package/src/settings/Sheet.vue +4 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@olares/lares-mobile",
|
|
3
|
-
"version": "0.15.
|
|
3
|
+
"version": "0.15.9",
|
|
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.9",
|
|
32
32
|
"markdown-it": "^14.3.0"
|
|
33
33
|
}
|
|
34
34
|
}
|
package/src/App.vue
CHANGED
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
:effort-name="effortName"
|
|
15
15
|
@create="newChat"
|
|
16
16
|
@pick-session="pickSession"
|
|
17
|
+
@pick-workspace="pickWorkspace"
|
|
17
18
|
@retry="retry"
|
|
18
19
|
@open-file="openFile"
|
|
19
20
|
@answer="answerQuestion"
|
|
@@ -77,16 +78,15 @@
|
|
|
77
78
|
@move-session="moveSession"
|
|
78
79
|
@rename-session="renameSession"
|
|
79
80
|
@archive-session="archiveSession"
|
|
80
|
-
@pick-workspace="pickWorkspace"
|
|
81
81
|
@retry="retry"
|
|
82
82
|
@open-file="openFile"
|
|
83
83
|
@answer="answerQuestion"
|
|
84
84
|
@update-draft="updateDraft"
|
|
85
85
|
@send="send"
|
|
86
|
+
@queue-action="updateQueue"
|
|
86
87
|
@open-model="openModelSheet"
|
|
87
88
|
@open-effort="openEffortSheet"
|
|
88
89
|
@files="attachFiles"
|
|
89
|
-
@voice="toggleVoice"
|
|
90
90
|
@hold-start="onHoldStart"
|
|
91
91
|
@hold-end="onHoldEnd"
|
|
92
92
|
@close-model="modelSheet = false"
|
|
@@ -98,6 +98,7 @@
|
|
|
98
98
|
@open-commands="loadCommands"
|
|
99
99
|
@open-references="loadReferences"
|
|
100
100
|
@pick-command="pickCommand"
|
|
101
|
+
@toggle-speak="toggleSpeak"
|
|
101
102
|
/>
|
|
102
103
|
</div>
|
|
103
104
|
</template>
|
|
@@ -108,6 +109,12 @@ import { connectChat } from "./runtime.js";
|
|
|
108
109
|
import { createT } from "./i18n.js";
|
|
109
110
|
import { failText, withPendingUser } from "./chat/format.js";
|
|
110
111
|
import { createVoiceCapture } from "./chat/voice.js";
|
|
112
|
+
import { createAnswerPlayer } from "./chat/playback.js";
|
|
113
|
+
import { createReplySpeaker } from "@olares/lares-core/voice/speech";
|
|
114
|
+
import { wavBlob } from "@olares/lares-core/voice/client";
|
|
115
|
+
import { captureSource } from "@olares/lares-core/voice/input";
|
|
116
|
+
import { browserPlays } from "@olares/lares-core/voice/playback";
|
|
117
|
+
import { speakProblemText, speakToggleNotice } from "@olares/lares-core/voice/problem";
|
|
111
118
|
import { appendDraftMentions } from "@olares/lares-core/files/mention";
|
|
112
119
|
import { FileIntake, partitionDocumentsBySize, splitComposerFiles } from "@olares/lares-core/files/intake";
|
|
113
120
|
import {
|
|
@@ -117,16 +124,28 @@ import {
|
|
|
117
124
|
draftImagesUploading,
|
|
118
125
|
} from "@olares/lares-core/files/draft-images";
|
|
119
126
|
import { DEFAULT_MAX_UPLOAD_BYTES } from "@olares/lares-core/files/limits";
|
|
120
|
-
import {
|
|
127
|
+
import {
|
|
128
|
+
groupModelsByProvider,
|
|
129
|
+
effortMenuRows,
|
|
130
|
+
reasoningOfModel,
|
|
131
|
+
currentEffortId,
|
|
132
|
+
selectionKey,
|
|
133
|
+
sessionModelSelection,
|
|
134
|
+
composerModelsFromCatalog,
|
|
135
|
+
modelSwitchSelection,
|
|
136
|
+
effortSwitchSelection,
|
|
137
|
+
} from "@olares/lares-core/router/session-model";
|
|
121
138
|
import { messageFromCode } from "@olares/lares-core/i18n/t";
|
|
122
139
|
import { parseAskUserQuestions, singleSelectAnswer } from "@olares/lares-core/larepass/questions";
|
|
123
140
|
import { applyCommand } from "@olares/lares-core/larepass/commands";
|
|
124
141
|
import { permissionOption, presetDescription, presetLabel } from "@olares/lares-core/larepass/permissions";
|
|
125
|
-
import { DEFAULT_BUSY_ENTER } from "@olares/lares-core/larepass/submission-settings";
|
|
142
|
+
import { DEFAULT_BUSY_ENTER, resolveSubmitMode } from "@olares/lares-core/larepass/submission-settings";
|
|
126
143
|
import { subscribeConversationSettings } from "@olares/lares-core/larepass/settings";
|
|
127
144
|
import { markSeen, pruneSeen, readSeen, SEEN_STORAGE_KEY, unseenSessions } from "@olares/lares-core/larepass/unread";
|
|
128
145
|
import { fileName } from "@olares/lares-core/files/filename";
|
|
129
146
|
import { closeTab, openTab, touchTab } from "@olares/lares-core/files/preview-tabs";
|
|
147
|
+
import { isFilesPath } from "@olares/lares-core/drive/files-path";
|
|
148
|
+
import { filesAppUrl } from "@olares/lares-core/files/app-link";
|
|
130
149
|
import { chatDevice as resolveChatDevice } from "./layout.js";
|
|
131
150
|
import LaresMobileShell from "./mobile/MobileShell.vue";
|
|
132
151
|
import LaresDesktopShell from "./desktop/DesktopShell.vue";
|
|
@@ -161,6 +180,8 @@ export default {
|
|
|
161
180
|
// Vue drops an undeclared prop into attrs, so a port missing from this list
|
|
162
181
|
// never reaches `ports` below no matter what the host passes.
|
|
163
182
|
socketProtocol: { type: [Function, String, Array], default: undefined },
|
|
183
|
+
// Set only by laresPortsFromAccount when myApps has no Lares app.
|
|
184
|
+
missing: { type: Boolean, default: false },
|
|
164
185
|
},
|
|
165
186
|
data() {
|
|
166
187
|
return {
|
|
@@ -168,8 +189,9 @@ export default {
|
|
|
168
189
|
stopping: false,
|
|
169
190
|
starting: false,
|
|
170
191
|
hostGen: 0,
|
|
171
|
-
|
|
192
|
+
modelCatalogGen: 0,
|
|
172
193
|
running: false,
|
|
194
|
+
runningSince: 0,
|
|
173
195
|
sessionId: "",
|
|
174
196
|
draft: "",
|
|
175
197
|
draftBySession: {},
|
|
@@ -189,18 +211,20 @@ export default {
|
|
|
189
211
|
sessions: [],
|
|
190
212
|
sessionsReady: false,
|
|
191
213
|
historyLoading: false,
|
|
192
|
-
|
|
214
|
+
modelCatalog: null,
|
|
193
215
|
modelError: "",
|
|
194
216
|
modelPending: "",
|
|
195
217
|
modelSheet: false,
|
|
196
218
|
effortSheet: false,
|
|
197
|
-
effortByModel: {},
|
|
198
219
|
upload: { pending: 0, failures: [] },
|
|
199
220
|
draftImages: [],
|
|
200
221
|
voicePhase: "idle",
|
|
201
222
|
voiceElapsed: 0,
|
|
202
223
|
voiceError: "",
|
|
203
224
|
voiceLanguage: "",
|
|
225
|
+
voiceSpeak: false,
|
|
226
|
+
speakBusy: false,
|
|
227
|
+
speakNotice: "",
|
|
204
228
|
intakeUnsub: null,
|
|
205
229
|
question: null,
|
|
206
230
|
questionBusy: false,
|
|
@@ -271,6 +295,7 @@ export default {
|
|
|
271
295
|
stopping: this.stopping,
|
|
272
296
|
starting: this.starting,
|
|
273
297
|
running: this.running,
|
|
298
|
+
runningSince: this.runningSince,
|
|
274
299
|
sessionId: this.sessionId,
|
|
275
300
|
sessionTitle: this.sessionTitle,
|
|
276
301
|
draft: this.draft,
|
|
@@ -284,6 +309,7 @@ export default {
|
|
|
284
309
|
previewMode: this.previewMode,
|
|
285
310
|
previewMediaSrc: this.previewMediaSrc,
|
|
286
311
|
previewDownloadHref: this.previewDownloadHref,
|
|
312
|
+
previewFilesHref: this.previewFilesHref,
|
|
287
313
|
panel: this.panel,
|
|
288
314
|
sessions: this.sessions,
|
|
289
315
|
sessionsReady: this.sessionsReady,
|
|
@@ -337,6 +363,9 @@ export default {
|
|
|
337
363
|
queueBusy: this.queueBusy,
|
|
338
364
|
busyEnter: this.busyEnter,
|
|
339
365
|
directory: this.directory,
|
|
366
|
+
voiceSpeak: this.voiceSpeak,
|
|
367
|
+
speakBusy: this.speakBusy,
|
|
368
|
+
speakNotice: this.speakNotice,
|
|
340
369
|
};
|
|
341
370
|
},
|
|
342
371
|
unseen() {
|
|
@@ -386,18 +415,21 @@ export default {
|
|
|
386
415
|
modelLabel() {
|
|
387
416
|
return this.currentListedModel?.name || this.models?.default?.model || this.t("model.select");
|
|
388
417
|
},
|
|
418
|
+
liveModelSelection() {
|
|
419
|
+
const row = this.sessions.find((item) => item.sessionId === this.sessionId);
|
|
420
|
+
return row?.modelSelection ?? null;
|
|
421
|
+
},
|
|
422
|
+
models() {
|
|
423
|
+
return composerModelsFromCatalog(
|
|
424
|
+
this.modelCatalog,
|
|
425
|
+
sessionModelSelection(this.liveModelSelection, this.modelCatalog?.default),
|
|
426
|
+
);
|
|
427
|
+
},
|
|
389
428
|
reasoning() {
|
|
390
429
|
return reasoningOfModel(this.currentListedModel);
|
|
391
430
|
},
|
|
392
431
|
effortId() {
|
|
393
|
-
|
|
394
|
-
return currentEffortId(this.models?.default, this.reasoning);
|
|
395
|
-
}
|
|
396
|
-
if (!(this.modelCurrent in this.effortByModel)) {
|
|
397
|
-
return currentEffortId(this.models?.default, this.reasoning);
|
|
398
|
-
}
|
|
399
|
-
const stored = this.effortByModel[this.modelCurrent];
|
|
400
|
-
return stored === "" ? undefined : stored;
|
|
432
|
+
return currentEffortId(this.models?.default, this.reasoning);
|
|
401
433
|
},
|
|
402
434
|
effortLabel() {
|
|
403
435
|
return this.effortName({ id: this.effortId, name: this.effortId });
|
|
@@ -422,15 +454,27 @@ export default {
|
|
|
422
454
|
previewMediaSrc() {
|
|
423
455
|
const data = this.preview.data;
|
|
424
456
|
if (!data) return "";
|
|
425
|
-
return this.runtime.mediaUrl(
|
|
457
|
+
return this.runtime.mediaUrl(
|
|
458
|
+
data.path,
|
|
459
|
+
data.modifiedAt,
|
|
460
|
+
data.kind === "image" ? "big" : undefined,
|
|
461
|
+
);
|
|
426
462
|
},
|
|
427
463
|
previewDownloadHref() {
|
|
428
464
|
if (!this.preview.path) return "";
|
|
429
465
|
return this.runtime.downloadUrl(this.preview.path);
|
|
430
466
|
},
|
|
467
|
+
previewFilesHref() {
|
|
468
|
+
if (!this.preview.path) return "";
|
|
469
|
+
return filesAppUrl(this.preview.path, {
|
|
470
|
+
entrance: this.baseUrl,
|
|
471
|
+
accountDomain: this.env?.ACCOUNT_DOMAIN,
|
|
472
|
+
protocol: this.env?.PROTOCOL,
|
|
473
|
+
});
|
|
474
|
+
},
|
|
431
475
|
previewHref() {
|
|
432
476
|
return (path) => {
|
|
433
|
-
const href = this.runtime.mediaUrl(path);
|
|
477
|
+
const href = this.runtime.mediaUrl(path, undefined, "big");
|
|
434
478
|
try {
|
|
435
479
|
return new URL(href, globalThis.location?.origin || "http://localhost").href;
|
|
436
480
|
} catch {
|
|
@@ -448,7 +492,7 @@ export default {
|
|
|
448
492
|
return this.models?.default ? selectionKey(this.models.default) : "";
|
|
449
493
|
},
|
|
450
494
|
modelBusy() {
|
|
451
|
-
return Boolean(this.modelPending) || (this.modelSheet && !this.
|
|
495
|
+
return Boolean(this.modelPending) || (this.modelSheet && !this.modelCatalog);
|
|
452
496
|
},
|
|
453
497
|
modelBlocked() {
|
|
454
498
|
return this.chatDevice === "desktop" && this.models?.routable === false;
|
|
@@ -474,28 +518,33 @@ export default {
|
|
|
474
518
|
created() {
|
|
475
519
|
// Per-path request tokens: reactive state would re-render on every read.
|
|
476
520
|
this.previewGen = new Map();
|
|
521
|
+
this.previewOpenGen = 0;
|
|
477
522
|
this.intake = new FileIntake((file, sessionId, options) => this.runtime.upload(file, options, sessionId));
|
|
478
523
|
this.imageUploadControllers = new Map();
|
|
524
|
+
this.player = createAnswerPlayer();
|
|
525
|
+
this.replySpeaker = createReplySpeaker();
|
|
526
|
+
this.speakGen = 0;
|
|
479
527
|
this.voiceCap = createVoiceCapture({
|
|
480
528
|
transcribe: (blob, signal) => this.runtime.transcribe(blob, this.voiceLanguage, signal),
|
|
481
|
-
getDraft: () => this.draft,
|
|
482
|
-
setDraft: (text) => {
|
|
483
|
-
this.draft = text;
|
|
484
|
-
},
|
|
485
529
|
onPhase: (phase, extra = {}) => {
|
|
486
530
|
this.voicePhase = phase;
|
|
487
531
|
if (extra.elapsed != null) this.voiceElapsed = extra.elapsed;
|
|
488
532
|
if (extra.error != null) this.voiceError = extra.error;
|
|
489
533
|
},
|
|
534
|
+
onTrace: (event, fields) => {
|
|
535
|
+
void this.runtime.reportLog?.("voice", event, fields);
|
|
536
|
+
},
|
|
537
|
+
submit: (text) => {
|
|
538
|
+
void this.runtime.reportLog?.("voice", "submit", {
|
|
539
|
+
chars: String(text ?? "").trim().length,
|
|
540
|
+
running: Boolean(this.running),
|
|
541
|
+
session: Boolean(this.sessionId),
|
|
542
|
+
});
|
|
543
|
+
void this.sendTranscript(text);
|
|
544
|
+
},
|
|
490
545
|
});
|
|
491
546
|
this.bindIntake(this.sessionId);
|
|
492
547
|
this.bindRuntime();
|
|
493
|
-
try {
|
|
494
|
-
const raw = JSON.parse(localStorage.getItem("lares.mobile.effort") || "{}");
|
|
495
|
-
if (raw && typeof raw === "object") this.effortByModel = raw;
|
|
496
|
-
} catch {
|
|
497
|
-
this.effortByModel = {};
|
|
498
|
-
}
|
|
499
548
|
try {
|
|
500
549
|
this.seen = readSeen(JSON.parse(localStorage.getItem(SEEN_STORAGE_KEY) || "null"));
|
|
501
550
|
} catch {
|
|
@@ -532,7 +581,11 @@ export default {
|
|
|
532
581
|
},
|
|
533
582
|
methods: {
|
|
534
583
|
mediaUrl(item) {
|
|
535
|
-
return this.runtime.mediaUrl(
|
|
584
|
+
return this.runtime.mediaUrl(
|
|
585
|
+
item.path,
|
|
586
|
+
item.modifiedAt,
|
|
587
|
+
item.kind === "image" ? "big" : undefined,
|
|
588
|
+
);
|
|
536
589
|
},
|
|
537
590
|
scrollTop(height, view) {
|
|
538
591
|
return this.runtime.scrollTop(height, view);
|
|
@@ -575,9 +628,21 @@ export default {
|
|
|
575
628
|
this.commands = null;
|
|
576
629
|
this.commandsError = "";
|
|
577
630
|
}
|
|
631
|
+
if (sessionChanged) {
|
|
632
|
+
this.speakGen += 1;
|
|
633
|
+
this.player?.stop();
|
|
634
|
+
}
|
|
635
|
+
const answer = this.replySpeaker.observe({
|
|
636
|
+
sessionChanged,
|
|
637
|
+
wasRunning: this.running,
|
|
638
|
+
running: snap.running,
|
|
639
|
+
reply: snap.reply,
|
|
640
|
+
});
|
|
578
641
|
this.sessionId = snap.sessionId;
|
|
579
642
|
this.items = snap.items;
|
|
580
643
|
this.running = snap.running;
|
|
644
|
+
this.runningSince = snap.runningSince || 0;
|
|
645
|
+
if (answer) void this.speakReply(answer, ++this.speakGen);
|
|
581
646
|
this.failed = snap.failed;
|
|
582
647
|
this.error = snap.error;
|
|
583
648
|
this.sessions = snap.sessions ?? this.sessions;
|
|
@@ -598,9 +663,6 @@ export default {
|
|
|
598
663
|
if (!this.question) this.questionBusy = false;
|
|
599
664
|
this.approval = snap.approval || null;
|
|
600
665
|
if (!this.approval) this.approvalBusy = false;
|
|
601
|
-
if (sessionChanged && this.chatDevice === "desktop") {
|
|
602
|
-
void this.loadSessionModels(snap.sessionId);
|
|
603
|
-
}
|
|
604
666
|
if (this.pendingUser && snap.items.some((row) => row.type === "user" && row.text === this.pendingUser)) {
|
|
605
667
|
this.pendingUser = "";
|
|
606
668
|
}
|
|
@@ -688,17 +750,19 @@ export default {
|
|
|
688
750
|
this.$refs.shell?.focusComposer?.();
|
|
689
751
|
},
|
|
690
752
|
async hydrateFiles(paths) {
|
|
691
|
-
const missing = [...new Set(paths)].filter((path) => !(
|
|
753
|
+
const missing = [...new Set(paths)].filter((path) => !Object.hasOwn(this.previews, path));
|
|
692
754
|
if (!missing.length) return;
|
|
693
|
-
const
|
|
755
|
+
const sessionId = this.sessionId;
|
|
756
|
+
const loaded = Object.create(null);
|
|
694
757
|
await Promise.all(missing.map(async (path) => {
|
|
695
758
|
try {
|
|
696
|
-
|
|
759
|
+
loaded[path] = await this.runtime.preview(path);
|
|
697
760
|
} catch {
|
|
698
|
-
|
|
761
|
+
loaded[path] = null;
|
|
699
762
|
}
|
|
700
763
|
}));
|
|
701
|
-
this.
|
|
764
|
+
if (this.sessionId !== sessionId) return;
|
|
765
|
+
this.previews = { ...this.previews, ...loaded };
|
|
702
766
|
this.pinLog();
|
|
703
767
|
},
|
|
704
768
|
async answerQuestion(label, row) {
|
|
@@ -735,13 +799,33 @@ export default {
|
|
|
735
799
|
this.approvalBusy = false;
|
|
736
800
|
}
|
|
737
801
|
},
|
|
738
|
-
|
|
802
|
+
openPreviewTab(path) {
|
|
739
803
|
const { tabs, lru, evicted } = openTab({ tabs: this.previewTabs, lru: this.previewLru }, path);
|
|
740
804
|
this.previewTabs = tabs;
|
|
741
805
|
this.previewLru = lru;
|
|
742
806
|
if (evicted) this.dropPreviewContent(evicted.path);
|
|
743
807
|
this.previewPath = path;
|
|
744
808
|
this.previewMode = "preview";
|
|
809
|
+
},
|
|
810
|
+
async openFile(path) {
|
|
811
|
+
const openGen = ++this.previewOpenGen;
|
|
812
|
+
if (isFilesPath(path)) {
|
|
813
|
+
const gen = (this.previewGen.get(path) ?? 0) + 1;
|
|
814
|
+
this.previewGen.set(path, gen);
|
|
815
|
+
try {
|
|
816
|
+
const data = await this.runtime.preview(path);
|
|
817
|
+
if (this.previewGen.get(path) !== gen || this.previewOpenGen !== openGen) return;
|
|
818
|
+
this.openPreviewTab(path);
|
|
819
|
+
const canonical = this.adoptPreviewPath(path, data?.path);
|
|
820
|
+
this.previewGen.set(canonical, gen);
|
|
821
|
+
this.setPreviewContent(canonical, gen, { status: "ready", data, error: "" });
|
|
822
|
+
} catch {
|
|
823
|
+
// Keep the Produced card in place so another click retries with the
|
|
824
|
+
// browser's then-current credential.
|
|
825
|
+
}
|
|
826
|
+
return;
|
|
827
|
+
}
|
|
828
|
+
this.openPreviewTab(path);
|
|
745
829
|
return this.loadPreview(path);
|
|
746
830
|
},
|
|
747
831
|
showChat() {
|
|
@@ -763,6 +847,7 @@ export default {
|
|
|
763
847
|
if (this.previewPath) this.closePreviewTab(this.previewPath);
|
|
764
848
|
},
|
|
765
849
|
resetPreview() {
|
|
850
|
+
this.previewOpenGen += 1;
|
|
766
851
|
this.previewTabs = [];
|
|
767
852
|
this.previewLru = [];
|
|
768
853
|
this.previewPath = "";
|
|
@@ -813,6 +898,13 @@ export default {
|
|
|
813
898
|
this.previewGen.set(canonical, gen);
|
|
814
899
|
this.setPreviewContent(canonical, gen, { status: "ready", data, error: "" });
|
|
815
900
|
} catch (err) {
|
|
901
|
+
if (
|
|
902
|
+
isFilesPath(path)
|
|
903
|
+
&& ["files_no_credential", "files_unauthenticated"].includes(err instanceof Error ? err.message : "")
|
|
904
|
+
) {
|
|
905
|
+
this.closePreviewTab(path);
|
|
906
|
+
return;
|
|
907
|
+
}
|
|
816
908
|
this.setPreviewContent(path, gen, {
|
|
817
909
|
status: "error",
|
|
818
910
|
data: null,
|
|
@@ -859,7 +951,7 @@ export default {
|
|
|
859
951
|
this.pendingUser = "";
|
|
860
952
|
this.failed = "";
|
|
861
953
|
this.error = "";
|
|
862
|
-
this.
|
|
954
|
+
this.modelCatalog = null;
|
|
863
955
|
this.modelError = "";
|
|
864
956
|
this.modelPending = "";
|
|
865
957
|
this.modelSheet = false;
|
|
@@ -912,58 +1004,36 @@ export default {
|
|
|
912
1004
|
},
|
|
913
1005
|
async loadComposerMeta() {
|
|
914
1006
|
const settings = this.runtime.settings;
|
|
915
|
-
|
|
916
|
-
if (this.chatDevice === "desktop") {
|
|
917
|
-
|
|
918
|
-
this.
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
this.busyEnter = DEFAULT_BUSY_ENTER;
|
|
923
|
-
}),
|
|
924
|
-
]);
|
|
925
|
-
} else {
|
|
926
|
-
try {
|
|
927
|
-
this.models = await settings.models();
|
|
928
|
-
this.modelError = "";
|
|
929
|
-
} catch (err) {
|
|
930
|
-
this.modelError = err instanceof Error ? err.message : String(err);
|
|
931
|
-
}
|
|
1007
|
+
const jobs = [this.loadModelCatalog()];
|
|
1008
|
+
if (settings && this.chatDevice === "desktop") {
|
|
1009
|
+
jobs.push(settings.conversation().then((section) => {
|
|
1010
|
+
this.busyEnter = section.busyEnter;
|
|
1011
|
+
}).catch(() => {
|
|
1012
|
+
this.busyEnter = DEFAULT_BUSY_ENTER;
|
|
1013
|
+
}));
|
|
932
1014
|
}
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
1015
|
+
if (settings) {
|
|
1016
|
+
jobs.push(settings.voice().then((voice) => {
|
|
1017
|
+
this.voiceLanguage = voice.config?.language || "";
|
|
1018
|
+
this.voiceSpeak = voice.config?.speak === true;
|
|
1019
|
+
}).catch(() => {
|
|
1020
|
+
this.voiceLanguage = "";
|
|
1021
|
+
this.voiceSpeak = false;
|
|
1022
|
+
}));
|
|
938
1023
|
}
|
|
1024
|
+
await Promise.all(jobs);
|
|
939
1025
|
},
|
|
940
|
-
async
|
|
941
|
-
const gen = ++this.
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
return;
|
|
945
|
-
}
|
|
946
|
-
this.modelError = "";
|
|
947
|
-
const loaded = await this.runtime.sessionModels(sessionId);
|
|
948
|
-
if (gen !== this.modelGen || sessionId !== this.sessionId) return;
|
|
1026
|
+
async loadModelCatalog() {
|
|
1027
|
+
const gen = ++this.modelCatalogGen;
|
|
1028
|
+
const loaded = await this.runtime.modelCatalog();
|
|
1029
|
+
if (gen !== this.modelCatalogGen) return;
|
|
949
1030
|
if (!loaded.ok) {
|
|
950
|
-
this.
|
|
1031
|
+
this.modelCatalog = null;
|
|
951
1032
|
this.modelError = loaded.error?.message || loaded.error?.code || "models";
|
|
952
1033
|
return;
|
|
953
1034
|
}
|
|
954
|
-
|
|
955
|
-
this.
|
|
956
|
-
default: value.current,
|
|
957
|
-
routable: value.routable !== false,
|
|
958
|
-
failures: value.failures ?? [],
|
|
959
|
-
models: (value.groups ?? []).flatMap((group) => (
|
|
960
|
-
(group.models ?? []).map((model) => ({
|
|
961
|
-
...model,
|
|
962
|
-
provider: group.id,
|
|
963
|
-
providerName: group.name || group.id,
|
|
964
|
-
}))
|
|
965
|
-
)),
|
|
966
|
-
};
|
|
1035
|
+
this.modelCatalog = loaded.value ?? null;
|
|
1036
|
+
this.modelError = "";
|
|
967
1037
|
},
|
|
968
1038
|
openEffortSheet() {
|
|
969
1039
|
if (this.effortDisabled) return;
|
|
@@ -974,71 +1044,34 @@ export default {
|
|
|
974
1044
|
const text = this.t(`effort.${row.id}`);
|
|
975
1045
|
return text === `effort.${row.id}` ? (row.name || row.id) : text;
|
|
976
1046
|
},
|
|
977
|
-
persistEffort() {
|
|
978
|
-
try {
|
|
979
|
-
localStorage.setItem("lares.mobile.effort", JSON.stringify(this.effortByModel));
|
|
980
|
-
} catch {
|
|
981
|
-
// storage may be blocked
|
|
982
|
-
}
|
|
983
|
-
},
|
|
984
1047
|
async chooseEffort(id) {
|
|
985
|
-
|
|
986
|
-
if (!this.
|
|
987
|
-
if (this.chatDevice === "desktop")
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
model: current.model,
|
|
995
|
-
...(id === undefined ? {} : { reasoningEffort: id }),
|
|
996
|
-
});
|
|
997
|
-
if (!selected.ok) {
|
|
998
|
-
this.modelError = selected.error?.message || selected.error?.code || "model";
|
|
999
|
-
return;
|
|
1000
|
-
}
|
|
1001
|
-
this.models = { ...this.models, default: selected.value?.selected ?? current };
|
|
1002
|
-
} finally {
|
|
1003
|
-
this.modelPending = "";
|
|
1048
|
+
const current = this.models?.default;
|
|
1049
|
+
if (!current || id === this.effortId || this.modelBusy) return;
|
|
1050
|
+
if (this.chatDevice === "desktop") this.effortSheet = false;
|
|
1051
|
+
this.modelPending = `${this.modelCurrent}\u0000${id ?? ""}`;
|
|
1052
|
+
this.modelError = "";
|
|
1053
|
+
try {
|
|
1054
|
+
const selected = await this.runtime.selectModel(effortSwitchSelection(current, id));
|
|
1055
|
+
if (!selected.ok) {
|
|
1056
|
+
this.modelError = selected.error?.message || selected.error?.code || "model";
|
|
1004
1057
|
}
|
|
1005
|
-
|
|
1058
|
+
} finally {
|
|
1059
|
+
this.modelPending = "";
|
|
1006
1060
|
}
|
|
1007
|
-
this.effortByModel = { ...this.effortByModel, [this.modelCurrent]: id ?? "" };
|
|
1008
|
-
this.persistEffort();
|
|
1009
1061
|
},
|
|
1010
1062
|
openModelSheet() {
|
|
1011
1063
|
this.modelSheet = true;
|
|
1012
|
-
if (!this.
|
|
1064
|
+
if (!this.modelCatalog) void this.loadModelCatalog();
|
|
1013
1065
|
},
|
|
1014
1066
|
async chooseModel(model) {
|
|
1015
1067
|
const key = this.modelKey(model);
|
|
1016
|
-
if (key === this.modelCurrent || this.modelBusy)
|
|
1017
|
-
|
|
1018
|
-
return;
|
|
1019
|
-
}
|
|
1020
|
-
this.modelSheet = false;
|
|
1068
|
+
if (key === this.modelCurrent || this.modelBusy) return;
|
|
1069
|
+
if (this.chatDevice === "desktop") this.modelSheet = false;
|
|
1021
1070
|
this.modelPending = key;
|
|
1022
1071
|
this.modelError = "";
|
|
1023
1072
|
try {
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
const selection = {
|
|
1027
|
-
provider: model.provider,
|
|
1028
|
-
model: model.id,
|
|
1029
|
-
...(reasoning?.defaultEffort === undefined
|
|
1030
|
-
? {}
|
|
1031
|
-
: { reasoningEffort: reasoning.defaultEffort }),
|
|
1032
|
-
};
|
|
1033
|
-
const selected = await this.runtime.selectModel(selection);
|
|
1034
|
-
if (!selected.ok) throw new Error(selected.error?.message || selected.error?.code || "model");
|
|
1035
|
-
this.models = { ...this.models, default: selected.value?.selected ?? selection };
|
|
1036
|
-
} else {
|
|
1037
|
-
this.models = await this.runtime.settings.setDefaultModel({
|
|
1038
|
-
provider: model.provider,
|
|
1039
|
-
model: model.id,
|
|
1040
|
-
});
|
|
1041
|
-
}
|
|
1073
|
+
const selected = await this.runtime.selectModel(modelSwitchSelection({ id: model.provider }, model));
|
|
1074
|
+
if (!selected.ok) throw new Error(selected.error?.message || selected.error?.code || "model");
|
|
1042
1075
|
} catch (err) {
|
|
1043
1076
|
this.modelError = err instanceof Error ? err.message : String(err);
|
|
1044
1077
|
} finally {
|
|
@@ -1136,15 +1169,71 @@ export default {
|
|
|
1136
1169
|
this.draftImages = [];
|
|
1137
1170
|
this.imagesBySession = {};
|
|
1138
1171
|
},
|
|
1139
|
-
onHoldStart() {
|
|
1140
|
-
|
|
1172
|
+
onHoldStart(detail) {
|
|
1173
|
+
void this.runtime.reportLog?.("voice", "hold-start", {
|
|
1174
|
+
via: detail?.via || "",
|
|
1175
|
+
phase: this.voicePhase,
|
|
1176
|
+
});
|
|
1177
|
+
if (captureSource("mobile") !== "device") return;
|
|
1178
|
+
if (this.voicePhase === "idle" || this.voicePhase === "error") void this.voiceCap?.start();
|
|
1179
|
+
this.speakGen += 1;
|
|
1180
|
+
this.player?.stop();
|
|
1181
|
+
this.player?.unlock();
|
|
1182
|
+
void this.runtime.interruptSpeech?.();
|
|
1183
|
+
},
|
|
1184
|
+
onHoldEnd(detail) {
|
|
1185
|
+
void this.runtime.reportLog?.("voice", "hold-end", {
|
|
1186
|
+
via: detail?.via || "",
|
|
1187
|
+
phase: this.voicePhase,
|
|
1188
|
+
});
|
|
1189
|
+
this.voiceCap?.stop(false);
|
|
1141
1190
|
},
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
else this.voiceCap?.stop(true);
|
|
1191
|
+
showSpeakProblem(err) {
|
|
1192
|
+
this.speakNotice = speakProblemText(this.t, err);
|
|
1145
1193
|
},
|
|
1146
|
-
|
|
1147
|
-
this.
|
|
1194
|
+
async toggleSpeak() {
|
|
1195
|
+
if (this.speakBusy) return;
|
|
1196
|
+
this.speakNotice = "";
|
|
1197
|
+
if (typeof this.runtime.settings?.saveVoice !== "function") {
|
|
1198
|
+
this.speakNotice = this.t("error.voice_speak_unready");
|
|
1199
|
+
return;
|
|
1200
|
+
}
|
|
1201
|
+
const next = !this.voiceSpeak;
|
|
1202
|
+
const previous = this.voiceSpeak;
|
|
1203
|
+
this.voiceSpeak = next;
|
|
1204
|
+
this.speakBusy = true;
|
|
1205
|
+
if (!next) {
|
|
1206
|
+
this.speakGen += 1;
|
|
1207
|
+
this.player?.stop();
|
|
1208
|
+
void this.runtime.interruptSpeech?.();
|
|
1209
|
+
}
|
|
1210
|
+
try {
|
|
1211
|
+
const voice = await this.runtime.settings.saveVoice({ speak: next });
|
|
1212
|
+
this.voiceSpeak = voice?.config ? voice.config.speak === true : next;
|
|
1213
|
+
const problem = this.voiceSpeak ? speakToggleNotice(voice?.ttsModels) : "";
|
|
1214
|
+
this.speakNotice = problem ? this.t(`error.${problem}`) : "";
|
|
1215
|
+
} catch (err) {
|
|
1216
|
+
this.voiceSpeak = previous;
|
|
1217
|
+
this.showSpeakProblem(err);
|
|
1218
|
+
} finally {
|
|
1219
|
+
this.speakBusy = false;
|
|
1220
|
+
}
|
|
1221
|
+
},
|
|
1222
|
+
async speakReply(text, gen) {
|
|
1223
|
+
if (!this.voiceSpeak) return;
|
|
1224
|
+
try {
|
|
1225
|
+
const result = await this.runtime.speak?.(text);
|
|
1226
|
+
if (gen !== this.speakGen) return;
|
|
1227
|
+
if (!result?.audio) {
|
|
1228
|
+
this.showSpeakProblem(Object.assign(new Error("voice_speak_failed"), { code: "voice_speak_failed" }));
|
|
1229
|
+
return;
|
|
1230
|
+
}
|
|
1231
|
+
if (!browserPlays("mobile", result.bluetooth)) return;
|
|
1232
|
+
await this.player?.play(wavBlob(result.audio));
|
|
1233
|
+
} catch (err) {
|
|
1234
|
+
if (gen !== this.speakGen) return;
|
|
1235
|
+
this.showSpeakProblem(err);
|
|
1236
|
+
}
|
|
1148
1237
|
},
|
|
1149
1238
|
toggleHistory() {
|
|
1150
1239
|
if (this.panel === "history") {
|
|
@@ -1290,9 +1379,31 @@ export default {
|
|
|
1290
1379
|
if (!sessionId) return;
|
|
1291
1380
|
return this.runtime.archiveSession(sessionId);
|
|
1292
1381
|
},
|
|
1382
|
+
async sendTranscript(text) {
|
|
1383
|
+
const spoken = String(text ?? "").trim();
|
|
1384
|
+
if (!spoken || !this.sessionId || this.modelBlocked || this.historyLoading || this.upload?.pending) return;
|
|
1385
|
+
const mode = resolveSubmitMode(this.running, false, true, this.busyEnter);
|
|
1386
|
+
this.sending = true;
|
|
1387
|
+
this.pendingUser = this.running ? "" : spoken;
|
|
1388
|
+
this.runtime.pinToBottom();
|
|
1389
|
+
this.pinLog();
|
|
1390
|
+
try {
|
|
1391
|
+
const sent = await this.runtime.send([{ type: "text", text: spoken }], mode);
|
|
1392
|
+
if (!sent.ok) {
|
|
1393
|
+
this.pendingUser = "";
|
|
1394
|
+
this.failed = sent.error?.message || sent.error?.code || "send";
|
|
1395
|
+
}
|
|
1396
|
+
} catch (err) {
|
|
1397
|
+
this.pendingUser = "";
|
|
1398
|
+
this.failed = err instanceof Error ? err.message : "send";
|
|
1399
|
+
} finally {
|
|
1400
|
+
this.sending = false;
|
|
1401
|
+
}
|
|
1402
|
+
},
|
|
1293
1403
|
async send(mode = "queue") {
|
|
1294
1404
|
const text = this.draft.trim();
|
|
1295
1405
|
if (!this.canSend) return;
|
|
1406
|
+
this.player?.unlock();
|
|
1296
1407
|
const sessionId = this.sessionId;
|
|
1297
1408
|
const images = this.draftImages.filter((row) => draftImageStatus(row) === "ready");
|
|
1298
1409
|
const wasRunning = this.running;
|