@meistrari/chat-nuxt 3.10.0 → 3.11.0
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/README.md +1 -1
- package/dist/module.json +1 -1
- package/dist/runtime/embed/components/chat-embed-inner.vue +93 -38
- package/dist/runtime/embed/components/mobile/chat-mobile-shell.vue +0 -1
- package/dist/runtime/embed/composables/use-agent-wake.d.ts +23 -0
- package/dist/runtime/embed/composables/use-agent-wake.js +182 -0
- package/dist/runtime/embed/feature-config.d.ts +4 -4
- package/dist/runtime/embed/feature-config.js +2 -1
- package/dist/runtime/embed/loading-messages.d.ts +16 -0
- package/dist/runtime/embed/loading-messages.js +10 -0
- package/dist/runtime/messages/components/chat-message-bubble.d.vue.ts +10 -1
- package/dist/runtime/messages/components/chat-message-bubble.vue +18 -7
- package/dist/runtime/messages/components/chat-message-bubble.vue.d.ts +10 -1
- package/dist/runtime/messages/components/chat-message-list.d.vue.ts +6 -1
- package/dist/runtime/messages/components/chat-message-list.vue +11 -2
- package/dist/runtime/messages/components/chat-message-list.vue.d.ts +6 -1
- package/dist/runtime/messages/components/reasoning/chat-reasoning-thought.d.vue.ts +2 -1
- package/dist/runtime/messages/components/reasoning/chat-reasoning-thought.vue +9 -8
- package/dist/runtime/messages/components/reasoning/chat-reasoning-thought.vue.d.ts +2 -1
- package/dist/runtime/messages/composables/chat.d.ts +8 -2
- package/dist/runtime/messages/composables/chat.js +180 -25
- package/dist/runtime/messages/composables/use-direct-stream.d.ts +41 -0
- package/dist/runtime/messages/composables/use-direct-stream.js +263 -0
- package/dist/runtime/messages/reasoning.d.ts +7 -0
- package/dist/runtime/messages/reasoning.js +5 -0
- package/dist/runtime/types/schemas/chat/conversations.d.ts +2 -0
- package/dist/runtime/types/schemas/chat/messages.d.ts +39 -0
- package/dist/runtime/types/schemas/chat/messages.js +24 -0
- package/dist/runtime/types/schemas/chat/models.d.ts +3 -2
- package/dist/runtime/types/schemas/chat/models.js +2 -1
- package/dist/runtime/types/schemas/chat/ui.d.ts +10 -0
- package/dist/runtime/types/schemas/chat/ui.js +1 -0
- package/dist/runtime/types/schemas/integrations/tela-agent-api.d.ts +35 -0
- package/dist/runtime/types/schemas/integrations/tela-agent-api.js +17 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -59,7 +59,7 @@ Declared in `src/runtime/embed/types.ts`; prop, feature-flag, and event payload
|
|
|
59
59
|
| `telaAgentInputs` | `TelaAgentExecutionInput[] \| null` | `undefined` | Inputs sent with each Tela agent run; hides the variables panel when non-null |
|
|
60
60
|
| `workspaceSettings` | `WorkspaceSettings \| null` | `null` | Override settings from host app; not accepted in Tela agent mode |
|
|
61
61
|
| `user` | `ChatActor \| null` | `null` | Override current user display info; not accepted in Tela agent mode |
|
|
62
|
-
| `features` | `Partial<ChatFeatureConfig>` | `{}` | Toggle optional features: `showUsageTab`, `showDebugOption`, `showCancelButton` (
|
|
62
|
+
| `features` | `Partial<ChatFeatureConfig>` | `{}` | Toggle optional features: `showUsageTab`, `showDebugOption`, `showCancelButton` (default `false`) and `showTtft` (default `true`) |
|
|
63
63
|
| `loadingMessages` | `readonly string[] \| null` | Default chat messages | Override pending-response messages |
|
|
64
64
|
| `loadingMessagesMode` | `'ordered' \| 'random' \| null` | `'ordered'` | Rotate custom messages in order or at random; only applies when `loadingMessages` has a non-empty message |
|
|
65
65
|
| `customComponents` | `Record<string, Component>` | `undefined` | Custom markdown renderers keyed by markdown node or tag name |
|
package/dist/module.json
CHANGED
|
@@ -18,6 +18,7 @@ import { useConversationFiles } from "../../files/composables/conversation-files
|
|
|
18
18
|
import { useConversationFilesPanel } from "../../files/composables/conversation-files-panel";
|
|
19
19
|
import { useConversations } from "../../conversations/composables/conversations";
|
|
20
20
|
import { useEmbedConfig } from "../composables/embed-config";
|
|
21
|
+
import { useAgentWake } from "../composables/use-agent-wake";
|
|
21
22
|
import { useFileDrop } from "../../files/composables/file-drop";
|
|
22
23
|
import { useFilePreviewPanel } from "../../files/composables/file-preview-panel";
|
|
23
24
|
import { usePendingConversationGroup } from "../../conversations/composables/pending-conversation-group";
|
|
@@ -155,16 +156,26 @@ const {
|
|
|
155
156
|
generatingTitle,
|
|
156
157
|
animatedTitle,
|
|
157
158
|
hasPendingMessage,
|
|
159
|
+
streamingActiveMessageId,
|
|
160
|
+
preparingSteps,
|
|
158
161
|
loadConversation,
|
|
159
162
|
setConversation,
|
|
163
|
+
adoptConversation,
|
|
160
164
|
setConversationModel,
|
|
161
165
|
pendingModelSelection,
|
|
162
166
|
sendMessage,
|
|
163
167
|
retryMessage,
|
|
164
168
|
cancelConversation,
|
|
165
169
|
startPolling,
|
|
166
|
-
clearConversation
|
|
170
|
+
clearConversation,
|
|
171
|
+
error: chatError
|
|
167
172
|
} = useChat();
|
|
173
|
+
const agentWake = useAgentWake();
|
|
174
|
+
const modelUpdateInFlight = ref(null);
|
|
175
|
+
watch(chatError, (message) => {
|
|
176
|
+
if (message)
|
|
177
|
+
statusToast.update({ text: message, icon: "i-ph-warning" });
|
|
178
|
+
});
|
|
168
179
|
const conversationIdRef = computed(() => currentConversation.value?.id ?? null);
|
|
169
180
|
const { total, files: conversationFiles } = useConversationFiles(conversationIdRef);
|
|
170
181
|
const selectedModel = computed({
|
|
@@ -175,14 +186,24 @@ const selectedModel = computed({
|
|
|
175
186
|
if (conversation && conversation.model !== value) {
|
|
176
187
|
const previousModel = conversation.model;
|
|
177
188
|
setConversationModel(value);
|
|
178
|
-
|
|
189
|
+
const update = updateConversationModel(conversation.id, value).then((updated) => {
|
|
179
190
|
if (!updated && currentConversation.value?.id === conversation.id && currentConversation.value.model === value) {
|
|
180
191
|
setConversationModel(previousModel);
|
|
181
192
|
}
|
|
193
|
+
return updated;
|
|
194
|
+
});
|
|
195
|
+
modelUpdateInFlight.value = update.finally(() => {
|
|
196
|
+
if (modelUpdateInFlight.value === update)
|
|
197
|
+
modelUpdateInFlight.value = null;
|
|
182
198
|
});
|
|
183
199
|
}
|
|
184
200
|
}
|
|
185
201
|
});
|
|
202
|
+
watch(selectedModel, (model, previousModel) => {
|
|
203
|
+
if (!isReady.value || model === previousModel || currentConversation.value?.threadSessionId)
|
|
204
|
+
return;
|
|
205
|
+
void agentWake.wake(model);
|
|
206
|
+
});
|
|
186
207
|
const filesCount = computed(() => total.value);
|
|
187
208
|
const hasMessages = computed(() => messages.value.length > 0);
|
|
188
209
|
const mobilePreviewFile = computed(() => {
|
|
@@ -222,6 +243,10 @@ function updateConversationId(nextConversationId) {
|
|
|
222
243
|
}
|
|
223
244
|
async function ensureConversationLoaded(conversationId) {
|
|
224
245
|
if (currentConversation.value?.id === conversationId) {
|
|
246
|
+
void agentWake.wake(
|
|
247
|
+
currentConversation.value.model,
|
|
248
|
+
currentConversation.value.threadSessionId ?? void 0
|
|
249
|
+
);
|
|
225
250
|
startPolling();
|
|
226
251
|
return;
|
|
227
252
|
}
|
|
@@ -232,12 +257,17 @@ async function ensureConversationLoaded(conversationId) {
|
|
|
232
257
|
try {
|
|
233
258
|
await loadConversation(conversationId);
|
|
234
259
|
if (currentConversation.value?.id !== conversationId) {
|
|
260
|
+
agentWake.skip();
|
|
235
261
|
statusToast.update({
|
|
236
262
|
text: "Erro ao carregar conversa",
|
|
237
263
|
icon: "i-ph-warning"
|
|
238
264
|
});
|
|
239
265
|
return;
|
|
240
266
|
}
|
|
267
|
+
void agentWake.wake(
|
|
268
|
+
currentConversation.value.model,
|
|
269
|
+
currentConversation.value.threadSessionId ?? void 0
|
|
270
|
+
);
|
|
241
271
|
startPolling();
|
|
242
272
|
} finally {
|
|
243
273
|
if (loadingConversationId.value === conversationId) {
|
|
@@ -247,6 +277,7 @@ async function ensureConversationLoaded(conversationId) {
|
|
|
247
277
|
}
|
|
248
278
|
function resetConversation(shouldEmit = true) {
|
|
249
279
|
clearConversation();
|
|
280
|
+
void agentWake.wake(selectedModel.value);
|
|
250
281
|
activeTab.value = "chat";
|
|
251
282
|
closeFilesPanel();
|
|
252
283
|
closePreview();
|
|
@@ -404,43 +435,55 @@ const conversationCreator = computed(
|
|
|
404
435
|
getMember
|
|
405
436
|
})
|
|
406
437
|
);
|
|
438
|
+
function claimableWakeSessionId() {
|
|
439
|
+
return agentWake.state.value === "ready" && !currentConversation.value?.threadSessionId ? agentWake.sessionId.value : void 0;
|
|
440
|
+
}
|
|
407
441
|
async function handleSend(content, files, agentInputs) {
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
442
|
+
const resolvedAgentInputs = resolveTelaAgentInputs(agentInputs, embedConfig?.telaAgentInputs?.value);
|
|
443
|
+
let claimedWakeSessionId;
|
|
444
|
+
const sent = await sendMessage(content, files.length > 0 ? files : void 0, resolvedAgentInputs, async (signal) => {
|
|
445
|
+
if (!activeConversationId.value || !currentConversation.value) {
|
|
446
|
+
const conversation2 = await createConversation({
|
|
447
|
+
model: selectedModel.value,
|
|
448
|
+
...resolvedAgentInputs !== void 0 ? { agentInputs: resolvedAgentInputs } : {}
|
|
449
|
+
});
|
|
450
|
+
if (!conversation2)
|
|
451
|
+
throw new Error("Erro ao criar conversa");
|
|
452
|
+
const groupId = pendingConversationGroupId.value;
|
|
453
|
+
clearPendingConversationGroup();
|
|
454
|
+
const grouped = groupId && await moveConversationToGroup(conversation2.id, groupId);
|
|
455
|
+
adoptConversation(grouped ? { ...conversation2, groupId } : conversation2);
|
|
456
|
+
updateConversationId(conversation2.id);
|
|
457
|
+
}
|
|
458
|
+
if (modelUpdateInFlight.value)
|
|
459
|
+
await modelUpdateInFlight.value;
|
|
460
|
+
const conversation = currentConversation.value;
|
|
418
461
|
if (!conversation)
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
462
|
+
throw new Error("Erro ao preparar conversa");
|
|
463
|
+
if (conversation.threadSessionId) {
|
|
464
|
+
await agentWake.wake(
|
|
465
|
+
conversation.model,
|
|
466
|
+
conversation.threadSessionId
|
|
467
|
+
);
|
|
425
468
|
} else {
|
|
426
|
-
|
|
469
|
+
await agentWake.waitUntilSettled(signal);
|
|
427
470
|
}
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
content,
|
|
434
|
-
files.length > 0 ? files : void 0,
|
|
435
|
-
resolvedAgentInputs
|
|
436
|
-
);
|
|
437
|
-
if (createdConversationId) {
|
|
438
|
-
updateConversationId(createdConversationId);
|
|
439
|
-
}
|
|
440
|
-
await sendPromise;
|
|
471
|
+
claimedWakeSessionId = claimableWakeSessionId();
|
|
472
|
+
return claimedWakeSessionId ? { wakeSessionId: claimedWakeSessionId } : {};
|
|
473
|
+
});
|
|
474
|
+
if (sent && claimedWakeSessionId)
|
|
475
|
+
agentWake.consume();
|
|
441
476
|
}
|
|
442
477
|
async function handleRetry(messageId) {
|
|
443
|
-
|
|
478
|
+
if (currentConversation.value?.threadSessionId) {
|
|
479
|
+
await agentWake.wake(
|
|
480
|
+
currentConversation.value.model,
|
|
481
|
+
currentConversation.value.threadSessionId
|
|
482
|
+
);
|
|
483
|
+
} else {
|
|
484
|
+
await agentWake.waitUntilSettled();
|
|
485
|
+
}
|
|
486
|
+
await retryMessage(messageId, claimableWakeSessionId());
|
|
444
487
|
}
|
|
445
488
|
async function handleNewConversation(groupId) {
|
|
446
489
|
setPendingConversationGroup(groupId);
|
|
@@ -765,7 +808,14 @@ async function handleDelete() {
|
|
|
765
808
|
@tab-change="handleTabChange"
|
|
766
809
|
>
|
|
767
810
|
<template #loading>
|
|
768
|
-
<ChatMessageList
|
|
811
|
+
<ChatMessageList
|
|
812
|
+
:messages="[]"
|
|
813
|
+
:streaming-active-message-id="streamingActiveMessageId"
|
|
814
|
+
:preparing-steps="preparingSteps"
|
|
815
|
+
:show-ttft="resolvedFeatures.showTtft"
|
|
816
|
+
loading
|
|
817
|
+
variant="mobile"
|
|
818
|
+
/>
|
|
769
819
|
<ChatMessageInput
|
|
770
820
|
v-model:selected-model="selectedModel"
|
|
771
821
|
:disabled="true"
|
|
@@ -798,7 +848,6 @@ async function handleDelete() {
|
|
|
798
848
|
<ChatMessageInput
|
|
799
849
|
ref="messageInputRef"
|
|
800
850
|
v-model:selected-model="selectedModel"
|
|
801
|
-
:disabled="hasPendingMessage"
|
|
802
851
|
:loading="hasPendingMessage"
|
|
803
852
|
:creator-name="currentUser?.name ?? void 0"
|
|
804
853
|
:creator-email="
|
|
@@ -823,13 +872,15 @@ async function handleDelete() {
|
|
|
823
872
|
|
|
824
873
|
<ChatMessageList
|
|
825
874
|
:messages="messages"
|
|
875
|
+
:streaming-active-message-id="streamingActiveMessageId"
|
|
876
|
+
:preparing-steps="preparingSteps"
|
|
877
|
+
:show-ttft="resolvedFeatures.showTtft"
|
|
826
878
|
variant="mobile"
|
|
827
879
|
@retry="handleRetry"
|
|
828
880
|
/>
|
|
829
881
|
<ChatMessageInput
|
|
830
882
|
ref="messageInputRef"
|
|
831
883
|
v-model:selected-model="selectedModel"
|
|
832
|
-
:disabled="hasPendingMessage"
|
|
833
884
|
:loading="hasPendingMessage"
|
|
834
885
|
:creator-name="currentUser?.name ?? void 0"
|
|
835
886
|
:creator-email="
|
|
@@ -950,6 +1001,9 @@ async function handleDelete() {
|
|
|
950
1001
|
>
|
|
951
1002
|
<ChatMessageList
|
|
952
1003
|
:messages="[]"
|
|
1004
|
+
:streaming-active-message-id="streamingActiveMessageId"
|
|
1005
|
+
:preparing-steps="preparingSteps"
|
|
1006
|
+
:show-ttft="resolvedFeatures.showTtft"
|
|
953
1007
|
loading
|
|
954
1008
|
:variant="compact ? 'widget' : 'page'"
|
|
955
1009
|
/>
|
|
@@ -991,7 +1045,6 @@ async function handleDelete() {
|
|
|
991
1045
|
<ChatMessageInput
|
|
992
1046
|
ref="messageInputRef"
|
|
993
1047
|
v-model:selected-model="selectedModel"
|
|
994
|
-
:disabled="hasPendingMessage"
|
|
995
1048
|
:loading="hasPendingMessage"
|
|
996
1049
|
:creator-name="currentUser?.name ?? void 0"
|
|
997
1050
|
:creator-email="
|
|
@@ -1019,13 +1072,15 @@ async function handleDelete() {
|
|
|
1019
1072
|
>
|
|
1020
1073
|
<ChatMessageList
|
|
1021
1074
|
:messages="messages"
|
|
1075
|
+
:streaming-active-message-id="streamingActiveMessageId"
|
|
1076
|
+
:preparing-steps="preparingSteps"
|
|
1077
|
+
:show-ttft="resolvedFeatures.showTtft"
|
|
1022
1078
|
:variant="compact ? 'widget' : 'page'"
|
|
1023
1079
|
@retry="handleRetry"
|
|
1024
1080
|
/>
|
|
1025
1081
|
<ChatMessageInput
|
|
1026
1082
|
ref="messageInputRef"
|
|
1027
1083
|
v-model:selected-model="selectedModel"
|
|
1028
|
-
:disabled="hasPendingMessage"
|
|
1029
1084
|
:loading="hasPendingMessage"
|
|
1030
1085
|
:creator-name="currentUser?.name ?? void 0"
|
|
1031
1086
|
:creator-email="
|
|
@@ -225,7 +225,6 @@ function startNewConversationFromOutdatedSettings() {
|
|
|
225
225
|
<ChatMessageInput
|
|
226
226
|
ref="homeInputRef"
|
|
227
227
|
v-model:selected-model="selectedModel"
|
|
228
|
-
:disabled="hasPendingMessage"
|
|
229
228
|
:loading="hasPendingMessage"
|
|
230
229
|
:creator-name="currentUser?.name ?? void 0"
|
|
231
230
|
:creator-email="currentUser?.email ?? void 0"
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { TelaAgentWakeResponse } from '../../types/schemas/integrations/tela-agent-api.js';
|
|
2
|
+
export type AgentWakeSnapshot = {
|
|
3
|
+
state: TelaAgentWakeResponse['status'] | 'idle';
|
|
4
|
+
sessionId?: string;
|
|
5
|
+
model?: string;
|
|
6
|
+
};
|
|
7
|
+
export declare function pollAgentWake(options: {
|
|
8
|
+
request: () => Promise<TelaAgentWakeResponse>;
|
|
9
|
+
onResponse?: (response: TelaAgentWakeResponse) => void;
|
|
10
|
+
signal?: AbortSignal;
|
|
11
|
+
pollIntervalMs?: number;
|
|
12
|
+
budgetMs?: number;
|
|
13
|
+
now?: () => number;
|
|
14
|
+
wait?: (delayMs: number, signal?: AbortSignal) => Promise<void>;
|
|
15
|
+
}): Promise<TelaAgentWakeResponse>;
|
|
16
|
+
export declare function useAgentWake(): {
|
|
17
|
+
state: any;
|
|
18
|
+
sessionId: any;
|
|
19
|
+
wake: (model?: string | null, requestedSessionId?: string) => Promise<TelaAgentWakeResponse>;
|
|
20
|
+
skip: () => void;
|
|
21
|
+
consume: () => void;
|
|
22
|
+
waitUntilSettled: (signal?: AbortSignal) => Promise<AgentWakeSnapshot>;
|
|
23
|
+
};
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import { useChatApi } from "../../shared/composables/chat-api.js";
|
|
2
|
+
const WAKE_POLL_INTERVAL_MS = 1500;
|
|
3
|
+
const WAKE_BUDGET_MS = 3e4;
|
|
4
|
+
const WAKE_SETTLE_TIMEOUT_MS = 35e3;
|
|
5
|
+
export async function pollAgentWake(options) {
|
|
6
|
+
const now = options.now ?? Date.now;
|
|
7
|
+
const deadline = now() + (options.budgetMs ?? WAKE_BUDGET_MS);
|
|
8
|
+
const wait = options.wait ?? ((delayMs, signal) => new Promise((resolve, reject) => {
|
|
9
|
+
let timer;
|
|
10
|
+
const onAbort = () => {
|
|
11
|
+
if (timer)
|
|
12
|
+
clearTimeout(timer);
|
|
13
|
+
reject(new DOMException("Aborted", "AbortError"));
|
|
14
|
+
};
|
|
15
|
+
timer = setTimeout(() => {
|
|
16
|
+
signal?.removeEventListener("abort", onAbort);
|
|
17
|
+
resolve();
|
|
18
|
+
}, delayMs);
|
|
19
|
+
if (signal?.aborted)
|
|
20
|
+
onAbort();
|
|
21
|
+
else
|
|
22
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
23
|
+
}));
|
|
24
|
+
try {
|
|
25
|
+
while (!options.signal?.aborted && now() <= deadline) {
|
|
26
|
+
const response = await options.request();
|
|
27
|
+
options.onResponse?.(response);
|
|
28
|
+
if (response.status !== "waking")
|
|
29
|
+
return response;
|
|
30
|
+
const remaining = deadline - now();
|
|
31
|
+
if (remaining <= 0)
|
|
32
|
+
break;
|
|
33
|
+
await wait(Math.min(options.pollIntervalMs ?? WAKE_POLL_INTERVAL_MS, remaining), options.signal);
|
|
34
|
+
}
|
|
35
|
+
} catch (error) {
|
|
36
|
+
if (error instanceof DOMException && error.name === "AbortError")
|
|
37
|
+
throw error;
|
|
38
|
+
return { status: "unavailable" };
|
|
39
|
+
}
|
|
40
|
+
return { status: "unavailable" };
|
|
41
|
+
}
|
|
42
|
+
export function useAgentWake() {
|
|
43
|
+
const chatApi = useChatApi();
|
|
44
|
+
const snapshot = ref({ state: "idle" });
|
|
45
|
+
const state = computed(() => snapshot.value.state);
|
|
46
|
+
const sessionId = computed(() => snapshot.value.sessionId);
|
|
47
|
+
let generation = 0;
|
|
48
|
+
let abortController = null;
|
|
49
|
+
let activeWake = null;
|
|
50
|
+
function setSnapshot(next) {
|
|
51
|
+
snapshot.value = next;
|
|
52
|
+
}
|
|
53
|
+
function skip() {
|
|
54
|
+
generation++;
|
|
55
|
+
abortController?.abort();
|
|
56
|
+
abortController = null;
|
|
57
|
+
activeWake = null;
|
|
58
|
+
setSnapshot({
|
|
59
|
+
state: "idle",
|
|
60
|
+
...snapshot.value.sessionId ? { sessionId: snapshot.value.sessionId } : {}
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
function consume() {
|
|
64
|
+
generation++;
|
|
65
|
+
abortController?.abort();
|
|
66
|
+
abortController = null;
|
|
67
|
+
activeWake = null;
|
|
68
|
+
setSnapshot({ state: "idle" });
|
|
69
|
+
}
|
|
70
|
+
async function wake(model, requestedSessionId) {
|
|
71
|
+
generation++;
|
|
72
|
+
const currentGeneration = generation;
|
|
73
|
+
abortController?.abort();
|
|
74
|
+
const controller = new AbortController();
|
|
75
|
+
abortController = controller;
|
|
76
|
+
let wakeSessionId = requestedSessionId ?? snapshot.value.sessionId;
|
|
77
|
+
setSnapshot({
|
|
78
|
+
state: "waking",
|
|
79
|
+
...wakeSessionId ? { sessionId: wakeSessionId } : {},
|
|
80
|
+
...model ? { model } : {}
|
|
81
|
+
});
|
|
82
|
+
if (import.meta.server) {
|
|
83
|
+
const unavailable = { status: "unavailable" };
|
|
84
|
+
setSnapshot({ state: unavailable.status });
|
|
85
|
+
return unavailable;
|
|
86
|
+
}
|
|
87
|
+
const run = async () => {
|
|
88
|
+
try {
|
|
89
|
+
const result = await pollAgentWake({
|
|
90
|
+
signal: controller.signal,
|
|
91
|
+
request: () => $fetch(
|
|
92
|
+
chatApi.path("/agent/wake"),
|
|
93
|
+
chatApi.withChatHeaders({
|
|
94
|
+
method: "POST",
|
|
95
|
+
signal: controller.signal,
|
|
96
|
+
body: {
|
|
97
|
+
...wakeSessionId ? { sessionId: wakeSessionId } : {},
|
|
98
|
+
...model ? { model } : {}
|
|
99
|
+
}
|
|
100
|
+
})
|
|
101
|
+
),
|
|
102
|
+
onResponse: (response) => {
|
|
103
|
+
if (currentGeneration !== generation)
|
|
104
|
+
return;
|
|
105
|
+
wakeSessionId = response.sessionId ?? wakeSessionId;
|
|
106
|
+
setSnapshot({
|
|
107
|
+
state: response.status,
|
|
108
|
+
...wakeSessionId ? { sessionId: wakeSessionId } : {},
|
|
109
|
+
...model ? { model } : {}
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
if (currentGeneration === generation) {
|
|
114
|
+
setSnapshot({
|
|
115
|
+
state: result.status,
|
|
116
|
+
...result.sessionId ? { sessionId: result.sessionId } : {},
|
|
117
|
+
...model ? { model } : {}
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
return result;
|
|
121
|
+
} catch (error) {
|
|
122
|
+
if (!(error instanceof DOMException && error.name === "AbortError") && currentGeneration === generation)
|
|
123
|
+
setSnapshot({ state: "unavailable" });
|
|
124
|
+
return { status: "unavailable" };
|
|
125
|
+
} finally {
|
|
126
|
+
if (abortController === controller)
|
|
127
|
+
abortController = null;
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
const pending = run();
|
|
131
|
+
activeWake = pending;
|
|
132
|
+
try {
|
|
133
|
+
return await pending;
|
|
134
|
+
} finally {
|
|
135
|
+
if (activeWake === pending)
|
|
136
|
+
activeWake = null;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
async function waitUntilSettled(signal) {
|
|
140
|
+
if (snapshot.value.state !== "waking" || !activeWake)
|
|
141
|
+
return snapshot.value;
|
|
142
|
+
const pending = activeWake;
|
|
143
|
+
await new Promise((resolve, reject) => {
|
|
144
|
+
let settled = false;
|
|
145
|
+
let timer;
|
|
146
|
+
let onAbort = () => {
|
|
147
|
+
};
|
|
148
|
+
const finish = (error) => {
|
|
149
|
+
if (settled)
|
|
150
|
+
return;
|
|
151
|
+
settled = true;
|
|
152
|
+
if (timer)
|
|
153
|
+
clearTimeout(timer);
|
|
154
|
+
signal?.removeEventListener("abort", onAbort);
|
|
155
|
+
if (error)
|
|
156
|
+
reject(error);
|
|
157
|
+
else
|
|
158
|
+
resolve();
|
|
159
|
+
};
|
|
160
|
+
onAbort = () => finish(new DOMException("Aborted", "AbortError"));
|
|
161
|
+
timer = setTimeout(() => finish(), WAKE_SETTLE_TIMEOUT_MS);
|
|
162
|
+
if (signal?.aborted)
|
|
163
|
+
onAbort();
|
|
164
|
+
else
|
|
165
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
166
|
+
pending.then(() => finish(), () => finish());
|
|
167
|
+
});
|
|
168
|
+
return snapshot.value;
|
|
169
|
+
}
|
|
170
|
+
onUnmounted(() => {
|
|
171
|
+
generation++;
|
|
172
|
+
abortController?.abort();
|
|
173
|
+
});
|
|
174
|
+
return {
|
|
175
|
+
state,
|
|
176
|
+
sessionId,
|
|
177
|
+
wake,
|
|
178
|
+
skip,
|
|
179
|
+
consume,
|
|
180
|
+
waitUntilSettled
|
|
181
|
+
};
|
|
182
|
+
}
|
|
@@ -1,7 +1,4 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Optional UI toggles the host passes per embed instance. All default to `false`;
|
|
3
|
-
* the host owns the policy (hardcoded, role-gated, or from a feature-flag provider).
|
|
4
|
-
*/
|
|
1
|
+
/** Optional UI toggles the host passes per embed instance. */
|
|
5
2
|
export type ChatFeatureConfig = {
|
|
6
3
|
/** Usage tab in the chat topbar, with per-conversation token and cost stats. */
|
|
7
4
|
showUsageTab?: boolean;
|
|
@@ -9,10 +6,13 @@ export type ChatFeatureConfig = {
|
|
|
9
6
|
showDebugOption?: boolean;
|
|
10
7
|
/** "Stop generation" button while a response streams. */
|
|
11
8
|
showCancelButton?: boolean;
|
|
9
|
+
/** Time to first token shown in assistant message metadata. Defaults to true. */
|
|
10
|
+
showTtft?: boolean;
|
|
12
11
|
};
|
|
13
12
|
export type ResolvedChatFeatureConfig = {
|
|
14
13
|
showUsageTab: boolean;
|
|
15
14
|
showDebugOption: boolean;
|
|
16
15
|
showCancelButton: boolean;
|
|
16
|
+
showTtft: boolean;
|
|
17
17
|
};
|
|
18
18
|
export declare function resolveChatFeatures(features?: Partial<ChatFeatureConfig>): ResolvedChatFeatureConfig;
|
|
@@ -2,6 +2,7 @@ export function resolveChatFeatures(features) {
|
|
|
2
2
|
return {
|
|
3
3
|
showUsageTab: features?.showUsageTab ?? false,
|
|
4
4
|
showDebugOption: features?.showDebugOption ?? false,
|
|
5
|
-
showCancelButton: features?.showCancelButton ?? false
|
|
5
|
+
showCancelButton: features?.showCancelButton ?? false,
|
|
6
|
+
showTtft: features?.showTtft ?? true
|
|
6
7
|
};
|
|
7
8
|
}
|
|
@@ -1,4 +1,20 @@
|
|
|
1
1
|
export declare const DEFAULT_CHAT_LOADING_MESSAGES: readonly ["Analisando sua pergunta", "Processando informações", "Consultando base de conhecimento", "Organizando ideias", "Elaborando resposta", "Refinando detalhes", "Verificando contexto", "Conectando conceitos", "Preparando resultado", "Quase lá", "Finalizando análise", "Estruturando resposta"];
|
|
2
|
+
/**
|
|
3
|
+
* Stand-in steps for a turn that has not produced its own reasoning yet. They
|
|
4
|
+
* accumulate, so the bubble reads as an agent working through the wait rather
|
|
5
|
+
* than as a loading placeholder. The user message is already delivered at this
|
|
6
|
+
* point, so the wait belongs to the answer.
|
|
7
|
+
*/
|
|
8
|
+
export declare const CHAT_PREPARING_STEPS: readonly ["Preparando o ambiente de trabalho", "Analisando a mensagem"];
|
|
9
|
+
/**
|
|
10
|
+
* The first delay is measured from the send, every later one from the step
|
|
11
|
+
* before it — so the sequence paces itself instead of racing a shared clock.
|
|
12
|
+
* Late enough that a turn answering promptly never shows a stand-in, and
|
|
13
|
+
* jittered so it does not look like a fixed-timer artifact.
|
|
14
|
+
*/
|
|
15
|
+
export declare const CHAT_PREPARING_STEP_DELAYS_MS: readonly [2500, 4000];
|
|
16
|
+
export declare const CHAT_PREPARING_STEP_JITTER_MS = 500;
|
|
17
|
+
export declare function nextChatPreparingStepDelay(stepIndex: number, random?: () => number): number;
|
|
2
18
|
export type ChatLoadingMessageMode = 'ordered' | 'random';
|
|
3
19
|
export type ChatLoadingMessagesInput = {
|
|
4
20
|
loadingMessages?: readonly string[] | null;
|
|
@@ -12,6 +12,16 @@ export const DEFAULT_CHAT_LOADING_MESSAGES = [
|
|
|
12
12
|
"Finalizando an\xE1lise",
|
|
13
13
|
"Estruturando resposta"
|
|
14
14
|
];
|
|
15
|
+
export const CHAT_PREPARING_STEPS = [
|
|
16
|
+
"Preparando o ambiente de trabalho",
|
|
17
|
+
"Analisando a mensagem"
|
|
18
|
+
];
|
|
19
|
+
export const CHAT_PREPARING_STEP_DELAYS_MS = [2500, 4e3];
|
|
20
|
+
export const CHAT_PREPARING_STEP_JITTER_MS = 500;
|
|
21
|
+
export function nextChatPreparingStepDelay(stepIndex, random = Math.random) {
|
|
22
|
+
const base = CHAT_PREPARING_STEP_DELAYS_MS[stepIndex] ?? CHAT_PREPARING_STEP_DELAYS_MS.at(-1);
|
|
23
|
+
return base + Math.floor(random() * CHAT_PREPARING_STEP_JITTER_MS);
|
|
24
|
+
}
|
|
15
25
|
function normalizeMessages(messages) {
|
|
16
26
|
if (!messages?.length)
|
|
17
27
|
return [];
|
|
@@ -1,11 +1,20 @@
|
|
|
1
1
|
import type { Message } from '../../types/schemas/chat/conversations.js';
|
|
2
2
|
type __VLS_Props = {
|
|
3
3
|
message: Message;
|
|
4
|
+
streamActive?: boolean;
|
|
5
|
+
/**
|
|
6
|
+
* Stand-in steps shown while this turn is still working towards its first
|
|
7
|
+
* output. They yield as soon as the run emits real reasoning or content.
|
|
8
|
+
*/
|
|
9
|
+
preparingSteps?: readonly string[] | null;
|
|
10
|
+
showTtft?: boolean;
|
|
4
11
|
};
|
|
5
12
|
declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {} & {
|
|
6
13
|
retry: (messageId: string) => any;
|
|
7
14
|
}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
|
|
8
15
|
onRetry?: ((messageId: string) => any) | undefined;
|
|
9
|
-
}>, {
|
|
16
|
+
}>, {
|
|
17
|
+
showTtft: boolean;
|
|
18
|
+
}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
10
19
|
declare const _default: typeof __VLS_export;
|
|
11
20
|
export default _default;
|
|
@@ -6,7 +6,10 @@ import { isPromptTooLongFailure } from "../../types/schemas/chat/messages.js";
|
|
|
6
6
|
import { parseContentWithFiles } from "../../files/content-detection";
|
|
7
7
|
import ChatVaultLink from "../../files/components/chat-vault-link.vue";
|
|
8
8
|
const props = defineProps({
|
|
9
|
-
message: { type: null, required: true }
|
|
9
|
+
message: { type: null, required: true },
|
|
10
|
+
streamActive: { type: Boolean, required: false },
|
|
11
|
+
preparingSteps: { type: [Array, null], required: false },
|
|
12
|
+
showTtft: { type: Boolean, required: false, default: true }
|
|
10
13
|
});
|
|
11
14
|
const emit = defineEmits(["retry"]);
|
|
12
15
|
const { user } = useChatAuth();
|
|
@@ -176,17 +179,24 @@ const hasDetectedFiles = computed(() => contentSegments.value.some((s) => s.type
|
|
|
176
179
|
</template>
|
|
177
180
|
|
|
178
181
|
<template v-else>
|
|
182
|
+
<!--
|
|
183
|
+
A live stream normally owns the pending state, so this block stays
|
|
184
|
+
out of its way. Stand-in steps are the exception: until the run
|
|
185
|
+
emits anything the stream has nothing to show, and that silence is
|
|
186
|
+
exactly what they cover.
|
|
187
|
+
-->
|
|
179
188
|
<ChatReasoningThought
|
|
180
|
-
v-if="hasReasoning || isPending"
|
|
189
|
+
v-if="hasReasoning || preparingSteps?.length && !message.content || isPending && !streamActive"
|
|
181
190
|
:reasoning-data="message.reasoningData ?? []"
|
|
182
191
|
:animate="wasEverPending"
|
|
183
192
|
:completed="!isPending"
|
|
193
|
+
:preparing-steps="preparingSteps"
|
|
184
194
|
:created-at="message.createdAt"
|
|
185
195
|
:updated-at="message.updatedAt"
|
|
186
196
|
/>
|
|
187
197
|
|
|
188
198
|
<div
|
|
189
|
-
v-if="!isPending && message.content"
|
|
199
|
+
v-if="!isPending && message.content || isPending && streamActive"
|
|
190
200
|
max-w-prose
|
|
191
201
|
:class="[
|
|
192
202
|
isFailed ? 'flex gap-12px items-start bg-subtle border border-solid rounded-12px w-full' : 'rounded-12px code-theme'
|
|
@@ -238,7 +248,7 @@ const hasDetectedFiles = computed(() => contentSegments.value.some((s) => s.type
|
|
|
238
248
|
:custom-components="mergedCustomComponents"
|
|
239
249
|
:custom-html-tags="mergedCustomHtmlTags"
|
|
240
250
|
:render-code-blocks-as-pre="true"
|
|
241
|
-
:typewriter="shouldAnimate"
|
|
251
|
+
:typewriter="isPending || shouldAnimate"
|
|
242
252
|
/>
|
|
243
253
|
</ClientOnly>
|
|
244
254
|
<ChatFilePreview
|
|
@@ -261,11 +271,12 @@ const hasDetectedFiles = computed(() => contentSegments.value.some((s) => s.type
|
|
|
261
271
|
:custom-components="mergedCustomComponents"
|
|
262
272
|
:custom-html-tags="mergedCustomHtmlTags"
|
|
263
273
|
:render-code-blocks-as-pre="true"
|
|
264
|
-
:typewriter="shouldAnimate"
|
|
274
|
+
:typewriter="isPending || shouldAnimate"
|
|
265
275
|
text-black-900
|
|
266
276
|
/>
|
|
267
277
|
</ClientOnly>
|
|
268
278
|
<div
|
|
279
|
+
v-if="!isPending"
|
|
269
280
|
mt-8px
|
|
270
281
|
flex
|
|
271
282
|
items-center
|
|
@@ -274,9 +285,9 @@ const hasDetectedFiles = computed(() => contentSegments.value.some((s) => s.type
|
|
|
274
285
|
text-tertiary
|
|
275
286
|
>
|
|
276
287
|
<span>{{ formatTime(message.updatedAt || message.createdAt) }}</span>
|
|
277
|
-
<TelaDotSeparator v-if="formattedTtft || hasMessageFeedback" />
|
|
288
|
+
<TelaDotSeparator v-if="showTtft && formattedTtft || hasMessageFeedback" />
|
|
278
289
|
<div flex items-center gap-8px>
|
|
279
|
-
<span v-if="formattedTtft" flex items-center gap-4px>
|
|
290
|
+
<span v-if="showTtft && formattedTtft" flex items-center gap-4px>
|
|
280
291
|
{{ formattedTtft }}
|
|
281
292
|
</span>
|
|
282
293
|
<ChatMessageFeedback v-if="hasMessageFeedback" :message="message" />
|