@meistrari/chat-nuxt 3.11.0 → 3.12.0-rc.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/assets/css/markstream.css +1 -1
- package/dist/runtime/embed/components/chat-embed-inner.vue +76 -36
- package/dist/runtime/embed/composables/use-agent-wake.d.ts +1 -2
- package/dist/runtime/embed/composables/use-agent-wake.js +19 -13
- package/dist/runtime/embed/feature-config.d.ts +2 -0
- package/dist/runtime/embed/feature-config.js +2 -1
- package/dist/runtime/embed/loading-messages.d.ts +1 -1
- package/dist/runtime/embed/loading-messages.js +1 -1
- package/dist/runtime/messages/components/chat-message-bubble.vue +4 -4
- package/dist/runtime/messages/components/chat-message-input.d.vue.ts +8 -2
- package/dist/runtime/messages/components/chat-message-input.vue +51 -2
- package/dist/runtime/messages/components/chat-message-input.vue.d.ts +8 -2
- package/dist/runtime/messages/components/chat-message-list.vue +3 -5
- package/dist/runtime/messages/components/reasoning/chat-reasoning-steps.vue +1 -1
- package/dist/runtime/messages/components/reasoning/chat-reasoning-thought.vue +14 -66
- package/dist/runtime/messages/composables/chat-message-terminal.js +3 -3
- package/dist/runtime/messages/composables/chat.d.ts +17 -3
- package/dist/runtime/messages/composables/chat.js +246 -23
- package/dist/runtime/messages/composables/outbound-queue.d.ts +23 -0
- package/dist/runtime/messages/composables/outbound-queue.js +28 -0
- package/dist/runtime/messages/composables/use-direct-stream.d.ts +7 -1
- package/dist/runtime/messages/composables/use-direct-stream.js +30 -7
- package/dist/runtime/server/utils/chat-api-proxy.js +3 -1
- package/dist/runtime/shared/chat-api-error.d.ts +7 -0
- package/dist/runtime/shared/chat-api-error.js +24 -4
- package/dist/runtime/types/schemas/chat/chat-config.d.ts +1 -0
- package/dist/runtime/types/schemas/chat/chat-config.js +1 -0
- package/dist/runtime/types/schemas/chat/messages.d.ts +14 -0
- package/dist/runtime/types/schemas/chat/messages.js +6 -1
- package/dist/runtime/types/schemas/chat/ui.d.ts +2 -2
- package/dist/runtime/types/schemas/chat/ui.js +2 -2
- package/dist/runtime/types/schemas/index.d.ts +1 -0
- package/dist/runtime/types/schemas/index.js +1 -0
- package/dist/runtime/types/schemas/integrations/tela-agent-api.d.ts +46 -0
- package/dist/runtime/types/schemas/integrations/tela-agent-api.js +26 -0
- package/package.json +2 -2
|
@@ -14,17 +14,15 @@ const SUCCESS_ANIMATION_DURATION_MS = 1e3;
|
|
|
14
14
|
const DOT_ANIMATION_INTERVAL_MS = 500;
|
|
15
15
|
const AUTO_COLLAPSE_DELAY_MS = 1500;
|
|
16
16
|
const TEXT_TRUNCATE_LENGTH = 40;
|
|
17
|
-
const isExpanded = ref(
|
|
17
|
+
const isExpanded = ref(!props.completed);
|
|
18
18
|
const userHasExpanded = ref(false);
|
|
19
19
|
const showingSuccess = ref(false);
|
|
20
20
|
const wasEverPending = ref(!props.completed);
|
|
21
|
-
const previewContainer = ref(null);
|
|
22
21
|
const dotCount = ref(0);
|
|
23
22
|
const elapsedTime = ref(0);
|
|
24
23
|
let dotInterval;
|
|
25
24
|
let timerInterval;
|
|
26
25
|
const storageKey = computed(() => `reasoning-timer-${new Date(props.createdAt).getTime()}`);
|
|
27
|
-
const showGradient = computed(() => !props.completed && !isExpanded.value);
|
|
28
26
|
const displayedReasoning = computed(
|
|
29
27
|
() => resolveDisplayedReasoning(props.reasoningData, props.preparingSteps)
|
|
30
28
|
);
|
|
@@ -74,26 +72,11 @@ const loadingText = computed(() => {
|
|
|
74
72
|
const dots = ".".repeat(dotCount.value);
|
|
75
73
|
return `${currentStepName.value}${dots}`;
|
|
76
74
|
});
|
|
77
|
-
function toggleExpanded() {
|
|
78
|
-
userHasExpanded.value = true;
|
|
79
|
-
isExpanded.value = !isExpanded.value;
|
|
80
|
-
}
|
|
81
|
-
function scheduleScroll() {
|
|
82
|
-
requestAnimationFrame(() => {
|
|
83
|
-
if (!previewContainer.value)
|
|
84
|
-
return;
|
|
85
|
-
previewContainer.value.scrollTop = previewContainer.value.scrollHeight;
|
|
86
|
-
});
|
|
87
|
-
}
|
|
88
|
-
watch(() => groupedReasoning.value.length, () => {
|
|
89
|
-
if (props.completed || isExpanded.value || !previewContainer.value) {
|
|
90
|
-
return;
|
|
91
|
-
}
|
|
92
|
-
scheduleScroll();
|
|
93
|
-
});
|
|
94
75
|
watch(() => props.completed, (newCompleted, oldCompleted) => {
|
|
95
76
|
if (!newCompleted) {
|
|
96
77
|
wasEverPending.value = true;
|
|
78
|
+
if (!userHasExpanded.value)
|
|
79
|
+
isExpanded.value = true;
|
|
97
80
|
}
|
|
98
81
|
if (newCompleted && !oldCompleted && wasEverPending.value) {
|
|
99
82
|
showingSuccess.value = true;
|
|
@@ -101,19 +84,13 @@ watch(() => props.completed, (newCompleted, oldCompleted) => {
|
|
|
101
84
|
showingSuccess.value = false;
|
|
102
85
|
if (!userHasExpanded.value) {
|
|
103
86
|
setTimeout(() => {
|
|
104
|
-
|
|
87
|
+
if (props.completed)
|
|
88
|
+
isExpanded.value = false;
|
|
105
89
|
}, AUTO_COLLAPSE_DELAY_MS);
|
|
106
90
|
}
|
|
107
91
|
}, SUCCESS_ANIMATION_DURATION_MS);
|
|
108
92
|
}
|
|
109
93
|
});
|
|
110
|
-
watch(isExpanded, (newExpanded, oldExpanded) => {
|
|
111
|
-
if (!newExpanded && oldExpanded) {
|
|
112
|
-
nextTick(() => {
|
|
113
|
-
scheduleScroll();
|
|
114
|
-
});
|
|
115
|
-
}
|
|
116
|
-
});
|
|
117
94
|
watch(() => props.completed, (isCompleted) => {
|
|
118
95
|
if (!isCompleted && !dotInterval) {
|
|
119
96
|
dotInterval = setInterval(() => {
|
|
@@ -150,11 +127,6 @@ onMounted(() => {
|
|
|
150
127
|
elapsedTime.value = Date.now() - startTime;
|
|
151
128
|
}
|
|
152
129
|
}
|
|
153
|
-
if (hasReasoning.value && !isExpanded.value) {
|
|
154
|
-
nextTick(() => {
|
|
155
|
-
scheduleScroll();
|
|
156
|
-
});
|
|
157
|
-
}
|
|
158
130
|
});
|
|
159
131
|
onUnmounted(() => {
|
|
160
132
|
if (dotInterval)
|
|
@@ -209,14 +181,15 @@ onUnmounted(() => {
|
|
|
209
181
|
key="loading-text"
|
|
210
182
|
class="body-14-regular flex items-center gap-8px"
|
|
211
183
|
>
|
|
212
|
-
<span
|
|
213
|
-
|
|
214
|
-
>
|
|
215
|
-
{{ hasReasoning ? currentStepName : loadingText }}
|
|
184
|
+
<span v-if="hasReasoning" class="text-gray-700">
|
|
185
|
+
{{ currentStepName }}
|
|
216
186
|
</span>
|
|
187
|
+
<TelaChatTextShimmer v-else>
|
|
188
|
+
{{ loadingText }}
|
|
189
|
+
</TelaChatTextShimmer>
|
|
217
190
|
<span
|
|
218
191
|
v-if="liveFormattedDuration"
|
|
219
|
-
|
|
192
|
+
body-12-regular text-secondary
|
|
220
193
|
>
|
|
221
194
|
({{ liveFormattedDuration }})
|
|
222
195
|
</span>
|
|
@@ -234,34 +207,9 @@ onUnmounted(() => {
|
|
|
234
207
|
</button>
|
|
235
208
|
</TelaCollapsibleTrigger>
|
|
236
209
|
|
|
237
|
-
<!--
|
|
238
|
-
<div
|
|
239
|
-
v-if="!completed && !isExpanded"
|
|
240
|
-
border="~ 0.5 default rounded-16px"
|
|
241
|
-
mt-8px mb-16px
|
|
242
|
-
cursor-pointer
|
|
243
|
-
@click="toggleExpanded"
|
|
244
|
-
>
|
|
245
|
-
<div
|
|
246
|
-
ref="previewContainer"
|
|
247
|
-
class="preview-container"
|
|
248
|
-
:class="{ 'with-gradient': showGradient }"
|
|
249
|
-
>
|
|
250
|
-
<ChatReasoningSteps
|
|
251
|
-
:reasoning-data="displayedReasoning"
|
|
252
|
-
:animate="animate && !completed"
|
|
253
|
-
:completed="completed"
|
|
254
|
-
:is-preview-mode="true"
|
|
255
|
-
/>
|
|
256
|
-
</div>
|
|
257
|
-
</div>
|
|
258
|
-
|
|
259
|
-
<!-- Expanded mode: Full content without gradient -->
|
|
210
|
+
<!-- Open while running, collapsible once done -->
|
|
260
211
|
<TelaCollapsibleContent>
|
|
261
|
-
<div
|
|
262
|
-
border="~ 0.5 default rounded-16px"
|
|
263
|
-
mb-16px
|
|
264
|
-
>
|
|
212
|
+
<div mb-8px>
|
|
265
213
|
<ChatReasoningSteps
|
|
266
214
|
:reasoning-data="displayedReasoning"
|
|
267
215
|
:animate="animate && !completed"
|
|
@@ -274,5 +222,5 @@ onUnmounted(() => {
|
|
|
274
222
|
</template>
|
|
275
223
|
|
|
276
224
|
<style scoped>
|
|
277
|
-
.
|
|
225
|
+
.static-gradient-text{color:#9da2aa}.loading-dot{animation:pulse 1.5s ease-in-out infinite}@keyframes pulse{0%,to{opacity:1;transform:scale(1)}50%{opacity:.5;transform:scale(.85)}}.success-check{animation:successFade .2s ease-out}@keyframes successFade{0%{opacity:0}to{opacity:1}}
|
|
278
226
|
</style>
|
|
@@ -11,15 +11,15 @@ export function terminalMessageTransitions(previousMessages, nextMessages) {
|
|
|
11
11
|
const transitions = [];
|
|
12
12
|
for (const message of nextMessages) {
|
|
13
13
|
const previous = previousById.get(message.id);
|
|
14
|
-
if (message.role !== "assistant" || previous
|
|
14
|
+
if (message.role !== "assistant" || !previous)
|
|
15
15
|
continue;
|
|
16
|
-
if (message.status === "completed" /* Completed */) {
|
|
16
|
+
if (previous.status === "pending" /* Pending */ && message.status === "completed" /* Completed */) {
|
|
17
17
|
transitions.push({
|
|
18
18
|
conversationId: message.conversationId,
|
|
19
19
|
messageId: message.id,
|
|
20
20
|
status: "completed" /* Completed */
|
|
21
21
|
});
|
|
22
|
-
} else if (message.status === "failed" /* Failed */) {
|
|
22
|
+
} else if (message.status === "failed" /* Failed */ && (previous.status === "pending" /* Pending */ || previous.status === "completed" /* Completed */)) {
|
|
23
23
|
transitions.push({
|
|
24
24
|
conversationId: message.conversationId,
|
|
25
25
|
messageId: message.id,
|
|
@@ -1,11 +1,16 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type DeepReadonly, type MaybeRefOrGetter } from 'vue';
|
|
2
2
|
import type { Conversation, ConversationWithMessages, Message } from '../../types/schemas/chat/conversations.js';
|
|
3
3
|
import type { FileUploadInput } from '../../types/schemas/chat/files.js';
|
|
4
4
|
import type { SendMessageBody } from '../../types/schemas/chat/messages.js';
|
|
5
5
|
import type { TelaAgentExecutionInput } from '../../types/schemas/integrations/tela-agent-api.js';
|
|
6
|
+
import { type QueuedSend } from './outbound-queue.js';
|
|
6
7
|
export declare function resolveTelaAgentInputs(messageInputs: TelaAgentExecutionInput[] | undefined, configuredInputs: DeepReadonly<TelaAgentExecutionInput[]> | null | undefined): SendMessageBody['agentInputs'];
|
|
7
8
|
export declare function resolveStreamingContent(authoritative: string, provisional: string): string;
|
|
8
|
-
export declare function useChat(
|
|
9
|
+
export declare function useChat(options?: {
|
|
10
|
+
outboundQueueEnabled?: MaybeRefOrGetter<boolean>;
|
|
11
|
+
onQueuedSendCancelled?: (send: QueuedSend) => void;
|
|
12
|
+
onQueuedSendFailed?: (send: QueuedSend) => void;
|
|
13
|
+
}): {
|
|
9
14
|
currentConversation: any;
|
|
10
15
|
messages: any;
|
|
11
16
|
loading: any;
|
|
@@ -13,6 +18,12 @@ export declare function useChat(): {
|
|
|
13
18
|
error: any;
|
|
14
19
|
isPolling: any;
|
|
15
20
|
hasPendingMessage: any;
|
|
21
|
+
composerLocked: any;
|
|
22
|
+
queuedMessageContent: any;
|
|
23
|
+
outboundOccupied: any;
|
|
24
|
+
sessionReady: any;
|
|
25
|
+
agentSessionStatus: any;
|
|
26
|
+
agentSessionRewakeable: any;
|
|
16
27
|
streamingActiveMessageId: any;
|
|
17
28
|
preparingSteps: any;
|
|
18
29
|
cancelling: any;
|
|
@@ -25,7 +36,10 @@ export declare function useChat(): {
|
|
|
25
36
|
pendingModelSelection: any;
|
|
26
37
|
sendMessage: (content: string, files?: FileUploadInput[], agentInputs?: TelaAgentExecutionInput[], prepare?: (signal: AbortSignal) => Promise<{
|
|
27
38
|
wakeSessionId?: string;
|
|
28
|
-
|
|
39
|
+
defer?: boolean;
|
|
40
|
+
}>, queuedSend?: QueuedSend) => Promise<Message | null>;
|
|
41
|
+
flushOutboundQueue: (wakeSessionId?: string) => Promise<Message | null>;
|
|
42
|
+
failOutboundQueue: (message: string) => boolean;
|
|
29
43
|
retryMessage: (messageId: string, wakeSessionId?: string) => Promise<boolean>;
|
|
30
44
|
cancelConversation: () => Promise<boolean>;
|
|
31
45
|
startPolling: () => void;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { useChatAuth } from "#chat-auth";
|
|
2
2
|
import * as Sentry from "@sentry/nuxt";
|
|
3
|
+
import { toValue } from "vue";
|
|
3
4
|
import { defaultChatModel } from "../../types/schemas/chat/models.js";
|
|
4
5
|
import {
|
|
5
6
|
createReasoningCache,
|
|
@@ -12,18 +13,20 @@ import {
|
|
|
12
13
|
restoreAssistantMessageAfterRetryFailure,
|
|
13
14
|
selectTitleGenerationMessages,
|
|
14
15
|
shouldTreatLoadedConversationTitleAsGenerated,
|
|
15
|
-
CANCELLATION_FAILED_TEXT
|
|
16
|
+
CANCELLATION_FAILED_TEXT,
|
|
17
|
+
GENERATION_FAILED_TEXT
|
|
16
18
|
} from "../message-state.js";
|
|
17
19
|
import { exportConversationToMarkdown } from "../../conversations/export.js";
|
|
18
20
|
import { resolveChatStateScope } from "../../conversations/scope.js";
|
|
19
|
-
import { getChatApiErrorCode, getChatApiErrorMessage } from "../../shared/chat-api-error.js";
|
|
21
|
+
import { getChatApiErrorCode, getChatApiErrorMessage, getChatApiErrorStatus } from "../../shared/chat-api-error.js";
|
|
20
22
|
import { useChatApi } from "../../shared/composables/chat-api.js";
|
|
21
23
|
import { useConversationFileActions } from "../../files/composables/conversation-files.js";
|
|
22
24
|
import { useEmbedConfig } from "../../embed/composables/embed-config.js";
|
|
23
25
|
import { CHAT_PREPARING_STEPS, nextChatPreparingStepDelay } from "../../embed/loading-messages.js";
|
|
24
26
|
import { usePolling } from "../../shared/composables/polling.js";
|
|
25
|
-
import { terminalMessageTransitions, useChatMessageTerminal } from "./chat-message-terminal.js";
|
|
27
|
+
import { ChatMessageStatus, terminalMessageTransitions, useChatMessageTerminal } from "./chat-message-terminal.js";
|
|
26
28
|
import { useDirectStream } from "./use-direct-stream.js";
|
|
29
|
+
import { useOutboundQueue } from "./outbound-queue.js";
|
|
27
30
|
const WORKSPACE_AGENT_NOT_READY_TEXT = "O agente deste workspace ainda n\xE3o foi configurado no Tela. Abra as configura\xE7\xF5es do workspace para criar o agente.";
|
|
28
31
|
export function resolveTelaAgentInputs(messageInputs, configuredInputs) {
|
|
29
32
|
const inputs = messageInputs ?? configuredInputs;
|
|
@@ -36,7 +39,7 @@ export function resolveStreamingContent(authoritative, provisional) {
|
|
|
36
39
|
return authoritative;
|
|
37
40
|
return authoritative || provisional;
|
|
38
41
|
}
|
|
39
|
-
export function useChat() {
|
|
42
|
+
export function useChat(options = {}) {
|
|
40
43
|
const chatApi = useChatApi();
|
|
41
44
|
const chatMessageTerminal = useChatMessageTerminal();
|
|
42
45
|
const embedConfig = useEmbedConfig();
|
|
@@ -67,6 +70,23 @@ export function useChat() {
|
|
|
67
70
|
};
|
|
68
71
|
}
|
|
69
72
|
});
|
|
73
|
+
const agentSessionStatusBuckets = useState("chat-agent-session-status", () => ({}));
|
|
74
|
+
const agentSessionExecutableBuckets = useState("chat-agent-session-executable", () => ({}));
|
|
75
|
+
const agentSessionRewakeableBuckets = useState("chat-agent-session-rewakeable", () => ({}));
|
|
76
|
+
const agentSessionStatus = computed({
|
|
77
|
+
get: () => agentSessionStatusBuckets.value[chatScope.value] ?? null,
|
|
78
|
+
set: (value) => agentSessionStatusBuckets.value = { ...agentSessionStatusBuckets.value, [chatScope.value]: value }
|
|
79
|
+
});
|
|
80
|
+
const agentSessionExecutable = computed({
|
|
81
|
+
get: () => agentSessionExecutableBuckets.value[chatScope.value] ?? false,
|
|
82
|
+
set: (value) => agentSessionExecutableBuckets.value = { ...agentSessionExecutableBuckets.value, [chatScope.value]: value }
|
|
83
|
+
});
|
|
84
|
+
const agentSessionRewakeable = computed({
|
|
85
|
+
get: () => agentSessionRewakeableBuckets.value[chatScope.value] ?? false,
|
|
86
|
+
set: (value) => agentSessionRewakeableBuckets.value = { ...agentSessionRewakeableBuckets.value, [chatScope.value]: value }
|
|
87
|
+
});
|
|
88
|
+
const outbound = useOutboundQueue(chatScope);
|
|
89
|
+
const outboundQueueEnabled = computed(() => toValue(options.outboundQueueEnabled) ?? false);
|
|
70
90
|
const loading = ref(false);
|
|
71
91
|
const sending = ref(false);
|
|
72
92
|
const gateHolding = ref(false);
|
|
@@ -104,11 +124,49 @@ export function useChat() {
|
|
|
104
124
|
const hasPendingMessage = computed(
|
|
105
125
|
() => messages.value.some((m) => m.status === "pending")
|
|
106
126
|
);
|
|
127
|
+
const sessionReady = computed(() => {
|
|
128
|
+
if (!currentConversation.value?.threadSessionId)
|
|
129
|
+
return null;
|
|
130
|
+
return agentSessionStatus.value === "waiting_messages" && agentSessionExecutable.value;
|
|
131
|
+
});
|
|
132
|
+
const composerLocked = computed(() => hasPendingMessage.value || outbound.occupied.value);
|
|
133
|
+
const queuedMessageContent = computed(() => outbound.slot.value?.content ?? null);
|
|
107
134
|
const streamingMessageId = ref(null);
|
|
135
|
+
const pendingStreamReconciliationIds = ref(/* @__PURE__ */ new Set());
|
|
108
136
|
let requestDurableRecovery = () => {
|
|
109
137
|
};
|
|
138
|
+
function optimisticallyCompleteStreamedMessage(result) {
|
|
139
|
+
if (result.status === "waiting_messages") {
|
|
140
|
+
agentSessionStatus.value = "waiting_messages";
|
|
141
|
+
agentSessionExecutable.value = true;
|
|
142
|
+
agentSessionRewakeable.value = false;
|
|
143
|
+
}
|
|
144
|
+
const messageId = streamingMessageId.value;
|
|
145
|
+
if (!messageId)
|
|
146
|
+
return;
|
|
147
|
+
const pendingMessage = messages.value.find(
|
|
148
|
+
(message) => message.id === messageId && message.role === "assistant" && message.status === ChatMessageStatus.Pending
|
|
149
|
+
);
|
|
150
|
+
if (!pendingMessage)
|
|
151
|
+
return;
|
|
152
|
+
pendingStreamReconciliationIds.value = /* @__PURE__ */ new Set([
|
|
153
|
+
...pendingStreamReconciliationIds.value,
|
|
154
|
+
messageId
|
|
155
|
+
]);
|
|
156
|
+
messages.value = messages.value.map((message) => message.id === messageId ? {
|
|
157
|
+
...message,
|
|
158
|
+
content: resolveStreamingContent(message.content, result.text),
|
|
159
|
+
status: ChatMessageStatus.Completed
|
|
160
|
+
} : message);
|
|
161
|
+
chatMessageTerminal.emit({
|
|
162
|
+
conversationId: pendingMessage.conversationId,
|
|
163
|
+
messageId,
|
|
164
|
+
status: ChatMessageStatus.Completed
|
|
165
|
+
});
|
|
166
|
+
}
|
|
110
167
|
const directStream = useDirectStream({
|
|
111
|
-
onResult:
|
|
168
|
+
onResult: optimisticallyCompleteStreamedMessage,
|
|
169
|
+
onTerminal: () => requestDurableRecovery(),
|
|
112
170
|
onDisconnect: () => requestDurableRecovery()
|
|
113
171
|
});
|
|
114
172
|
const streamingActiveMessageId = computed(
|
|
@@ -121,12 +179,16 @@ export function useChat() {
|
|
|
121
179
|
const content = directStream.streamingText.value;
|
|
122
180
|
if (!messageId || !content)
|
|
123
181
|
return resolved;
|
|
124
|
-
return resolved.map((message) => message.id === messageId && message.status === "pending" ? { ...message, content: resolveStreamingContent(message.content, content) } : message);
|
|
182
|
+
return resolved.map((message) => message.id === messageId && (message.status === "pending" || pendingStreamReconciliationIds.value.has(message.id)) ? { ...message, content: resolveStreamingContent(message.content, content) } : message);
|
|
125
183
|
});
|
|
126
|
-
const stopCondition = computed(
|
|
184
|
+
const stopCondition = computed(
|
|
185
|
+
() => !hasPendingMessage.value && pendingStreamReconciliationIds.value.size === 0
|
|
186
|
+
);
|
|
127
187
|
let pollGeneration = 0;
|
|
128
188
|
const pollMessages = async () => {
|
|
129
|
-
if (!currentConversation.value
|
|
189
|
+
if (!currentConversation.value)
|
|
190
|
+
return;
|
|
191
|
+
if (gateHolding.value && !outboundQueueEnabled.value)
|
|
130
192
|
return;
|
|
131
193
|
const generation = pollGeneration;
|
|
132
194
|
try {
|
|
@@ -136,9 +198,34 @@ export function useChat() {
|
|
|
136
198
|
);
|
|
137
199
|
if (generation !== pollGeneration)
|
|
138
200
|
return;
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
201
|
+
if (response.agentSession) {
|
|
202
|
+
agentSessionStatus.value = response.agentSession.status;
|
|
203
|
+
agentSessionExecutable.value = response.agentSession.executable;
|
|
204
|
+
agentSessionRewakeable.value = response.agentSession.rewakeable ?? false;
|
|
205
|
+
}
|
|
206
|
+
if (gateHolding.value)
|
|
207
|
+
return;
|
|
208
|
+
const currentMessagesById = new Map(messages.value.map((message) => [message.id, message]));
|
|
209
|
+
const remainingReconciliationIds = new Set(pendingStreamReconciliationIds.value);
|
|
210
|
+
const durableMessageIds = new Set(response.messages.map((message) => message.id));
|
|
211
|
+
for (const messageId of remainingReconciliationIds) {
|
|
212
|
+
if (!durableMessageIds.has(messageId))
|
|
213
|
+
remainingReconciliationIds.delete(messageId);
|
|
214
|
+
}
|
|
215
|
+
const reconciledMessages = response.messages.map((message) => {
|
|
216
|
+
if (!remainingReconciliationIds.has(message.id))
|
|
217
|
+
return message;
|
|
218
|
+
if (message.status === ChatMessageStatus.Completed || message.status === ChatMessageStatus.Failed) {
|
|
219
|
+
remainingReconciliationIds.delete(message.id);
|
|
220
|
+
return message;
|
|
221
|
+
}
|
|
222
|
+
const optimisticMessage = currentMessagesById.get(message.id);
|
|
223
|
+
return optimisticMessage?.status === ChatMessageStatus.Completed ? optimisticMessage : message;
|
|
224
|
+
});
|
|
225
|
+
const terminalMessages = terminalMessageTransitions(messages.value, reconciledMessages);
|
|
226
|
+
pendingStreamReconciliationIds.value = remainingReconciliationIds;
|
|
227
|
+
messages.value = reconciledMessages;
|
|
228
|
+
const latestSessionId = reconciledMessages.findLast((message) => message.agentSessionId)?.agentSessionId;
|
|
142
229
|
if (latestSessionId && currentConversation.value.threadSessionId === null) {
|
|
143
230
|
currentConversation.value = {
|
|
144
231
|
...currentConversation.value,
|
|
@@ -148,7 +235,7 @@ export function useChat() {
|
|
|
148
235
|
for (const terminalMessage of terminalMessages)
|
|
149
236
|
chatMessageTerminal.emit(terminalMessage);
|
|
150
237
|
if (response.newFiles && response.newFiles.length > 0) {
|
|
151
|
-
const lastAssistantMessage =
|
|
238
|
+
const lastAssistantMessage = reconciledMessages.findLast((m) => m.role === "assistant");
|
|
152
239
|
if (lastAssistantMessage) {
|
|
153
240
|
addFilesToConversation(
|
|
154
241
|
currentConversation.value.id,
|
|
@@ -165,7 +252,22 @@ export function useChat() {
|
|
|
165
252
|
const errorRecord = err && typeof err === "object" ? err : null;
|
|
166
253
|
const statusCode = typeof errorRecord?.statusCode === "number" ? errorRecord.statusCode : typeof errorRecord?.status === "number" ? errorRecord.status : typeof errorRecord?.response?.status === "number" ? errorRecord.response.status : null;
|
|
167
254
|
if (statusCode && statusCode >= 400 && statusCode < 500 && statusCode !== 408 && statusCode !== 429) {
|
|
168
|
-
|
|
255
|
+
const previousMessages = messages.value;
|
|
256
|
+
const failedMessages = markPendingAssistantMessagesFailed(previousMessages).map((message) => {
|
|
257
|
+
if (message.role !== "assistant" || message.status !== ChatMessageStatus.Completed || !pendingStreamReconciliationIds.value.has(message.id)) {
|
|
258
|
+
return message;
|
|
259
|
+
}
|
|
260
|
+
return {
|
|
261
|
+
...message,
|
|
262
|
+
content: GENERATION_FAILED_TEXT,
|
|
263
|
+
status: ChatMessageStatus.Failed
|
|
264
|
+
};
|
|
265
|
+
});
|
|
266
|
+
const terminalMessages = terminalMessageTransitions(previousMessages, failedMessages);
|
|
267
|
+
pendingStreamReconciliationIds.value = /* @__PURE__ */ new Set();
|
|
268
|
+
messages.value = failedMessages;
|
|
269
|
+
for (const terminalMessage of terminalMessages)
|
|
270
|
+
chatMessageTerminal.emit(terminalMessage);
|
|
169
271
|
error.value = getChatApiErrorMessage(err) ?? "Erro ao atualizar mensagens";
|
|
170
272
|
stopPolling();
|
|
171
273
|
}
|
|
@@ -219,6 +321,10 @@ export function useChat() {
|
|
|
219
321
|
const conversationMessages = "messages" in conv ? conv.messages : [];
|
|
220
322
|
currentConversation.value = { ...conv, messages: conversationMessages };
|
|
221
323
|
messages.value = conversationMessages;
|
|
324
|
+
pendingStreamReconciliationIds.value = /* @__PURE__ */ new Set();
|
|
325
|
+
agentSessionStatus.value = null;
|
|
326
|
+
agentSessionExecutable.value = false;
|
|
327
|
+
agentSessionRewakeable.value = false;
|
|
222
328
|
titleGenerated.value = false;
|
|
223
329
|
error.value = null;
|
|
224
330
|
};
|
|
@@ -233,6 +339,7 @@ export function useChat() {
|
|
|
233
339
|
);
|
|
234
340
|
currentConversation.value = data;
|
|
235
341
|
messages.value = data.messages;
|
|
342
|
+
pendingStreamReconciliationIds.value = /* @__PURE__ */ new Set();
|
|
236
343
|
if (shouldTreatLoadedConversationTitleAsGenerated(data.messages)) {
|
|
237
344
|
titleGenerated.value = true;
|
|
238
345
|
}
|
|
@@ -255,16 +362,20 @@ export function useChat() {
|
|
|
255
362
|
loading.value = false;
|
|
256
363
|
}
|
|
257
364
|
};
|
|
258
|
-
const sendMessage = async (content, files, agentInputs, prepare) => {
|
|
365
|
+
const sendMessage = async (content, files, agentInputs, prepare, queuedSend) => {
|
|
259
366
|
if (!currentConversation.value && !prepare)
|
|
260
367
|
return null;
|
|
368
|
+
if (outboundQueueEnabled.value && outbound.occupied.value && !queuedSend)
|
|
369
|
+
return null;
|
|
261
370
|
sending.value = true;
|
|
262
371
|
error.value = null;
|
|
263
372
|
sendStartedAt = Date.now();
|
|
264
373
|
schedulePreparingSteps();
|
|
265
374
|
const filesWithSource = files?.map((f) => ({ ...f, source: "user" })) ?? null;
|
|
266
375
|
const conversationId = currentConversation.value?.id ?? "";
|
|
267
|
-
const
|
|
376
|
+
const existingOptimisticUserMessage = queuedSend ? messages.value.find((message) => message.id === queuedSend.optimisticIds.user) : void 0;
|
|
377
|
+
const existingOptimisticAssistantMessage = queuedSend ? messages.value.find((message) => message.id === queuedSend.optimisticIds.assistant) : void 0;
|
|
378
|
+
const optimisticUserMessage = existingOptimisticUserMessage ?? {
|
|
268
379
|
id: crypto.randomUUID(),
|
|
269
380
|
conversationId,
|
|
270
381
|
role: "user",
|
|
@@ -278,7 +389,7 @@ export function useChat() {
|
|
|
278
389
|
createdAt: /* @__PURE__ */ new Date(),
|
|
279
390
|
updatedAt: null
|
|
280
391
|
};
|
|
281
|
-
const optimisticAssistantMessage = {
|
|
392
|
+
const optimisticAssistantMessage = existingOptimisticAssistantMessage ?? {
|
|
282
393
|
id: crypto.randomUUID(),
|
|
283
394
|
conversationId,
|
|
284
395
|
role: "assistant",
|
|
@@ -294,15 +405,19 @@ export function useChat() {
|
|
|
294
405
|
};
|
|
295
406
|
streamingMessageId.value = optimisticAssistantMessage.id;
|
|
296
407
|
directStream.beginMessage();
|
|
297
|
-
|
|
408
|
+
if (!queuedSend)
|
|
409
|
+
messages.value = [...messages.value, optimisticUserMessage, optimisticAssistantMessage];
|
|
298
410
|
const controller = new AbortController();
|
|
299
411
|
gateController = controller;
|
|
300
412
|
try {
|
|
301
|
-
let wakeSessionId;
|
|
413
|
+
let wakeSessionId = queuedSend?.wakeSessionId;
|
|
414
|
+
let defer = false;
|
|
302
415
|
if (prepare) {
|
|
303
416
|
gateHolding.value = true;
|
|
304
417
|
try {
|
|
305
|
-
|
|
418
|
+
const prepared = await prepare(controller.signal);
|
|
419
|
+
wakeSessionId = prepared.wakeSessionId;
|
|
420
|
+
defer = prepared.defer === true;
|
|
306
421
|
} finally {
|
|
307
422
|
gateHolding.value = false;
|
|
308
423
|
}
|
|
@@ -311,6 +426,21 @@ export function useChat() {
|
|
|
311
426
|
throw new DOMException("Aborted", "AbortError");
|
|
312
427
|
if (!currentConversation.value)
|
|
313
428
|
throw new Error("Erro ao enviar mensagem");
|
|
429
|
+
const sessionReady2 = currentConversation.value.threadSessionId ? agentSessionStatus.value === "waiting_messages" && agentSessionExecutable.value : null;
|
|
430
|
+
if (outboundQueueEnabled.value && !queuedSend && (defer || sessionReady2 === false)) {
|
|
431
|
+
outbound.enqueue({
|
|
432
|
+
content,
|
|
433
|
+
files,
|
|
434
|
+
agentInputs,
|
|
435
|
+
wakeSessionId,
|
|
436
|
+
optimisticIds: { user: optimisticUserMessage.id, assistant: optimisticAssistantMessage.id },
|
|
437
|
+
queuedAt: Date.now(),
|
|
438
|
+
attempts: 0
|
|
439
|
+
});
|
|
440
|
+
gateHolding.value = true;
|
|
441
|
+
startPolling();
|
|
442
|
+
return optimisticUserMessage;
|
|
443
|
+
}
|
|
314
444
|
const resolvedAgentInputs = resolveTelaAgentInputs(agentInputs, embedConfig?.telaAgentInputs?.value);
|
|
315
445
|
const body = {
|
|
316
446
|
content,
|
|
@@ -347,7 +477,10 @@ export function useChat() {
|
|
|
347
477
|
acceptMessagePair
|
|
348
478
|
);
|
|
349
479
|
acceptedByStream = true;
|
|
350
|
-
} catch {
|
|
480
|
+
} catch (streamError) {
|
|
481
|
+
const statusCode = getChatApiErrorStatus(streamError);
|
|
482
|
+
if (statusCode !== null && statusCode >= 400 && statusCode < 500)
|
|
483
|
+
throw streamError;
|
|
351
484
|
response = await $fetch(
|
|
352
485
|
chatApi.path(`/conversations/${currentConversation.value.id}/messages`),
|
|
353
486
|
chatApi.withChatHeaders({ method: "POST", body })
|
|
@@ -366,6 +499,30 @@ export function useChat() {
|
|
|
366
499
|
startPolling();
|
|
367
500
|
return response.userMessage;
|
|
368
501
|
} catch (err) {
|
|
502
|
+
const code = getChatApiErrorCode(err);
|
|
503
|
+
const queuedAttempts = (queuedSend?.attempts ?? 0) + 1;
|
|
504
|
+
const queuedAgeMs = Date.now() - (queuedSend?.queuedAt ?? Date.now());
|
|
505
|
+
if (outboundQueueEnabled.value && (code === "turn_in_progress" || code === "sandbox_not_woken") && queuedAttempts < 3 && queuedAgeMs <= 9e4) {
|
|
506
|
+
outbound.replace({
|
|
507
|
+
content,
|
|
508
|
+
files,
|
|
509
|
+
agentInputs,
|
|
510
|
+
wakeSessionId: queuedSend?.wakeSessionId,
|
|
511
|
+
optimisticIds: { user: optimisticUserMessage.id, assistant: optimisticAssistantMessage.id },
|
|
512
|
+
queuedAt: queuedSend?.queuedAt ?? Date.now(),
|
|
513
|
+
attempts: queuedAttempts
|
|
514
|
+
});
|
|
515
|
+
if (code === "sandbox_not_woken") {
|
|
516
|
+
agentSessionExecutable.value = false;
|
|
517
|
+
agentSessionRewakeable.value = true;
|
|
518
|
+
} else {
|
|
519
|
+
agentSessionStatus.value = "running";
|
|
520
|
+
agentSessionExecutable.value = false;
|
|
521
|
+
}
|
|
522
|
+
gateHolding.value = true;
|
|
523
|
+
startPolling();
|
|
524
|
+
return optimisticUserMessage;
|
|
525
|
+
}
|
|
369
526
|
messages.value = messages.value.filter(
|
|
370
527
|
(msg) => msg.id !== optimisticUserMessage.id && msg.id !== optimisticAssistantMessage.id
|
|
371
528
|
);
|
|
@@ -377,27 +534,84 @@ export function useChat() {
|
|
|
377
534
|
return null;
|
|
378
535
|
const message = getChatApiErrorCode(err) === "workspace_agent_not_ready" ? WORKSPACE_AGENT_NOT_READY_TEXT : err instanceof Error ? err.message : "Erro ao enviar mensagem";
|
|
379
536
|
error.value = message;
|
|
537
|
+
if (outboundQueueEnabled.value && queuedSend)
|
|
538
|
+
options.onQueuedSendFailed?.(queuedSend);
|
|
380
539
|
Sentry.captureException(err);
|
|
381
540
|
return null;
|
|
382
541
|
} finally {
|
|
383
542
|
sending.value = false;
|
|
384
|
-
gateHolding.value =
|
|
543
|
+
gateHolding.value = outbound.occupied.value;
|
|
385
544
|
if (gateController === controller)
|
|
386
545
|
gateController = null;
|
|
387
546
|
}
|
|
388
547
|
};
|
|
548
|
+
let outboundFlushInFlight = null;
|
|
549
|
+
function flushOutboundQueue(wakeSessionId) {
|
|
550
|
+
if (outboundFlushInFlight)
|
|
551
|
+
return outboundFlushInFlight;
|
|
552
|
+
const queued = outbound.take();
|
|
553
|
+
if (!queued)
|
|
554
|
+
return Promise.resolve(null);
|
|
555
|
+
gateHolding.value = false;
|
|
556
|
+
const send = wakeSessionId ? { ...queued, wakeSessionId } : queued;
|
|
557
|
+
const pending = sendMessage(send.content, send.files, send.agentInputs, void 0, send);
|
|
558
|
+
outboundFlushInFlight = pending;
|
|
559
|
+
void pending.finally(() => {
|
|
560
|
+
if (outboundFlushInFlight === pending)
|
|
561
|
+
outboundFlushInFlight = null;
|
|
562
|
+
});
|
|
563
|
+
return pending;
|
|
564
|
+
}
|
|
565
|
+
function failOutboundQueue(message) {
|
|
566
|
+
const queued = outbound.clear();
|
|
567
|
+
if (!queued)
|
|
568
|
+
return false;
|
|
569
|
+
messages.value = messages.value.filter(
|
|
570
|
+
(item) => item.id !== queued.optimisticIds.user && item.id !== queued.optimisticIds.assistant
|
|
571
|
+
);
|
|
572
|
+
gateHolding.value = false;
|
|
573
|
+
streamingMessageId.value = null;
|
|
574
|
+
clearPreparingSteps();
|
|
575
|
+
directStream.disconnect();
|
|
576
|
+
stopPolling();
|
|
577
|
+
error.value = message;
|
|
578
|
+
options.onQueuedSendFailed?.(queued);
|
|
579
|
+
return true;
|
|
580
|
+
}
|
|
581
|
+
watch(sessionReady, (ready) => {
|
|
582
|
+
if (ready === true && outbound.occupied.value)
|
|
583
|
+
void flushOutboundQueue();
|
|
584
|
+
});
|
|
389
585
|
const clearConversation = () => {
|
|
390
586
|
stopPolling();
|
|
391
587
|
gateController?.abort();
|
|
392
588
|
streamingMessageId.value = null;
|
|
393
589
|
clearPreparingSteps();
|
|
394
590
|
directStream.disconnect();
|
|
591
|
+
outbound.clear();
|
|
395
592
|
currentConversation.value = null;
|
|
396
593
|
messages.value = [];
|
|
594
|
+
pendingStreamReconciliationIds.value = /* @__PURE__ */ new Set();
|
|
397
595
|
error.value = null;
|
|
596
|
+
agentSessionStatus.value = null;
|
|
597
|
+
agentSessionExecutable.value = false;
|
|
598
|
+
agentSessionRewakeable.value = false;
|
|
398
599
|
};
|
|
399
600
|
const cancelling = ref(false);
|
|
400
601
|
const cancelConversation = async () => {
|
|
602
|
+
const queued = outbound.clear();
|
|
603
|
+
if (queued) {
|
|
604
|
+
messages.value = messages.value.filter(
|
|
605
|
+
(message) => message.id !== queued.optimisticIds.user && message.id !== queued.optimisticIds.assistant
|
|
606
|
+
);
|
|
607
|
+
gateHolding.value = false;
|
|
608
|
+
streamingMessageId.value = null;
|
|
609
|
+
clearPreparingSteps();
|
|
610
|
+
directStream.disconnect();
|
|
611
|
+
stopPolling();
|
|
612
|
+
options.onQueuedSendCancelled?.(queued);
|
|
613
|
+
return true;
|
|
614
|
+
}
|
|
401
615
|
if (gateHolding.value) {
|
|
402
616
|
gateController?.abort();
|
|
403
617
|
return true;
|
|
@@ -445,6 +659,7 @@ export function useChat() {
|
|
|
445
659
|
messages: []
|
|
446
660
|
};
|
|
447
661
|
messages.value = [];
|
|
662
|
+
pendingStreamReconciliationIds.value = /* @__PURE__ */ new Set();
|
|
448
663
|
streamingMessageId.value = null;
|
|
449
664
|
directStream.disconnect();
|
|
450
665
|
return true;
|
|
@@ -548,8 +763,8 @@ export function useChat() {
|
|
|
548
763
|
messages: messages.value
|
|
549
764
|
});
|
|
550
765
|
};
|
|
551
|
-
watch(
|
|
552
|
-
if (
|
|
766
|
+
watch(stopCondition, (settled, wasSettled) => {
|
|
767
|
+
if (settled && !wasSettled) {
|
|
553
768
|
streamingMessageId.value = null;
|
|
554
769
|
directStream.disconnect();
|
|
555
770
|
void generateTitle();
|
|
@@ -566,6 +781,12 @@ export function useChat() {
|
|
|
566
781
|
error: readonly(error),
|
|
567
782
|
isPolling: readonly(isPolling),
|
|
568
783
|
hasPendingMessage,
|
|
784
|
+
composerLocked,
|
|
785
|
+
queuedMessageContent,
|
|
786
|
+
outboundOccupied: outbound.occupied,
|
|
787
|
+
sessionReady,
|
|
788
|
+
agentSessionStatus: readonly(agentSessionStatus),
|
|
789
|
+
agentSessionRewakeable: readonly(agentSessionRewakeable),
|
|
569
790
|
streamingActiveMessageId,
|
|
570
791
|
preparingSteps: readonly(preparingSteps),
|
|
571
792
|
cancelling: readonly(cancelling),
|
|
@@ -577,6 +798,8 @@ export function useChat() {
|
|
|
577
798
|
setConversationModel,
|
|
578
799
|
pendingModelSelection,
|
|
579
800
|
sendMessage,
|
|
801
|
+
flushOutboundQueue,
|
|
802
|
+
failOutboundQueue,
|
|
580
803
|
retryMessage,
|
|
581
804
|
cancelConversation,
|
|
582
805
|
startPolling,
|