@meistrari/chat-nuxt 4.10.0-rc.8 → 4.10.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 +8 -6
- package/dist/module.d.mts +0 -7
- package/dist/module.json +1 -1
- package/dist/module.mjs +1 -2
- package/dist/runtime/conversations/components/chat-conversation-export-modal.d.vue.ts +10 -0
- package/dist/runtime/conversations/components/chat-conversation-export-modal.vue +142 -0
- package/dist/runtime/conversations/components/chat-conversation-export-modal.vue.d.ts +10 -0
- package/dist/runtime/conversations/components/chat-conversation-list.vue +8 -1
- package/dist/runtime/conversations/components/chat-export-document.d.vue.ts +7 -0
- package/dist/runtime/conversations/components/chat-export-document.vue +26 -0
- package/dist/runtime/conversations/components/chat-export-document.vue.d.ts +7 -0
- package/dist/runtime/conversations/components/chat-export-file.d.vue.ts +9 -0
- package/dist/runtime/conversations/components/chat-export-file.vue +12 -0
- package/dist/runtime/conversations/components/chat-export-file.vue.d.ts +9 -0
- package/dist/runtime/conversations/components/chat-export-message.d.vue.ts +7 -0
- package/dist/runtime/conversations/components/chat-export-message.vue +84 -0
- package/dist/runtime/conversations/components/chat-export-message.vue.d.ts +7 -0
- package/dist/runtime/conversations/components/chat-export-openui.d.vue.ts +9 -0
- package/dist/runtime/conversations/components/chat-export-openui.vue +19 -0
- package/dist/runtime/conversations/components/chat-export-openui.vue.d.ts +9 -0
- package/dist/runtime/conversations/components/chat-export-questions.d.vue.ts +19 -0
- package/dist/runtime/conversations/components/chat-export-questions.vue +22 -0
- package/dist/runtime/conversations/components/chat-export-questions.vue.d.ts +19 -0
- package/dist/runtime/conversations/composables/conversation-list.d.ts +2 -1
- package/dist/runtime/conversations/composables/conversation-list.js +4 -13
- package/dist/runtime/conversations/export-document.d.ts +1 -0
- package/dist/runtime/conversations/export-document.js +154 -0
- package/dist/runtime/conversations/export.d.ts +4 -0
- package/dist/runtime/conversations/export.js +69 -4
- package/dist/runtime/embed/components/chat-embed-inner.vue +93 -8
- package/dist/runtime/embed/components/mobile/chat-mobile-drawer.d.vue.ts +2 -2
- package/dist/runtime/embed/components/mobile/chat-mobile-drawer.vue.d.ts +2 -2
- package/dist/runtime/embed/components/mobile/chat-mobile-shell.d.vue.ts +5 -4
- package/dist/runtime/embed/components/mobile/chat-mobile-shell.vue +2 -0
- package/dist/runtime/embed/components/mobile/chat-mobile-shell.vue.d.ts +5 -4
- package/dist/runtime/embed/execution-controls.d.ts +2 -1
- package/dist/runtime/embed/execution-controls.js +2 -2
- package/dist/runtime/messages/components/chat-execution-mode-selector.vue +10 -6
- package/dist/runtime/messages/components/chat-message-bubble.vue +7 -0
- package/dist/runtime/messages/components/chat-message-input.d.vue.ts +2 -0
- package/dist/runtime/messages/components/chat-message-input.vue +18 -1
- package/dist/runtime/messages/components/chat-message-input.vue.d.ts +2 -0
- package/dist/runtime/messages/composables/chat.d.ts +15 -0
- package/dist/runtime/messages/composables/chat.js +59 -8
- package/dist/runtime/messages/composer-draft.d.ts +16 -0
- package/dist/runtime/messages/composer-draft.js +61 -0
- package/dist/runtime/messages/composer-pending-send.d.ts +8 -0
- package/dist/runtime/messages/composer-pending-send.js +34 -0
- package/dist/runtime/messages/conversation-timeline.js +1 -1
- package/dist/runtime/server/utils/chat-api-proxy.d.ts +1 -1
- package/dist/runtime/server/utils/chat-api-proxy.js +2 -4
- package/dist/runtime/types/composer.d.ts +6 -0
- package/dist/runtime/types/schemas/chat/v3.d.ts +83 -0
- package/dist/runtime/types/schemas/chat/v3.js +28 -1
- package/dist/runtime/types/schemas/integrations/agent-events.d.ts +16 -0
- package/dist/runtime/types/schemas/integrations/agent-events.js +12 -0
- package/dist/runtime/types/schemas/integrations/slack-apps.d.ts +53 -0
- package/dist/runtime/types/schemas/integrations/slack-apps.js +6 -0
- package/dist/runtime/types/schemas/integrations/slack-identity.d.ts +26 -0
- package/dist/runtime/types/schemas/integrations/slack-identity.js +23 -0
- package/dist/runtime/types/schemas/integrations/tela-api.d.ts +11 -0
- package/dist/runtime/types/schemas/integrations/tela-api.js +8 -0
- package/dist/runtime/types/schemas/integrations/tela-auth.d.ts +39 -0
- package/dist/runtime/types/schemas/integrations/tela-auth.js +12 -0
- package/dist/runtime/types/schemas/slack/apps.d.ts +144 -0
- package/dist/runtime/types/schemas/slack/apps.js +53 -0
- package/dist/runtime/types/schemas/slack/identities.d.ts +58 -0
- package/dist/runtime/types/schemas/slack/identities.js +19 -0
- package/dist/runtime/types/schemas/slack/installations.d.ts +39 -0
- package/dist/runtime/types/schemas/slack/installations.js +18 -0
- package/package.json +2 -2
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
export async function captureConversationDocument(root, signal, includeImages) {
|
|
2
|
+
const deadline = Date.now() + 15e3;
|
|
3
|
+
const resourceSignal = AbortSignal.any([signal, AbortSignal.timeout(15e3)]);
|
|
4
|
+
async function waitForResource(promise) {
|
|
5
|
+
resourceSignal.throwIfAborted();
|
|
6
|
+
let onAbort = () => {
|
|
7
|
+
};
|
|
8
|
+
try {
|
|
9
|
+
return await Promise.race([
|
|
10
|
+
promise,
|
|
11
|
+
new Promise((_resolve, reject) => {
|
|
12
|
+
onAbort = () => reject(resourceSignal.reason);
|
|
13
|
+
resourceSignal.addEventListener("abort", onAbort, { once: true });
|
|
14
|
+
})
|
|
15
|
+
]);
|
|
16
|
+
} finally {
|
|
17
|
+
resourceSignal.removeEventListener("abort", onAbort);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
await waitForResource(document.fonts.ready);
|
|
21
|
+
let previousHtml = "";
|
|
22
|
+
while (true) {
|
|
23
|
+
signal.throwIfAborted();
|
|
24
|
+
if (!root.isConnected)
|
|
25
|
+
throw new Error("A prepara\xE7\xE3o da conversa foi interrompida.");
|
|
26
|
+
if (Date.now() > deadline)
|
|
27
|
+
throw new Error("Alguns elementos n\xE3o terminaram de carregar. Tente exportar novamente.");
|
|
28
|
+
const pending = root.querySelector('[data-chat-file="loading"], [data-export-loading], [aria-busy="true"]');
|
|
29
|
+
const html = root.innerHTML;
|
|
30
|
+
if (!pending && html === previousHtml)
|
|
31
|
+
break;
|
|
32
|
+
previousHtml = html;
|
|
33
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
34
|
+
}
|
|
35
|
+
const copy = root.cloneNode(true);
|
|
36
|
+
const originals = [root, ...root.querySelectorAll("*")];
|
|
37
|
+
const copies = [copy, ...copy.querySelectorAll("*")];
|
|
38
|
+
const properties = [
|
|
39
|
+
"display",
|
|
40
|
+
"color",
|
|
41
|
+
"background-color",
|
|
42
|
+
"font-size",
|
|
43
|
+
"font-weight",
|
|
44
|
+
"font-style",
|
|
45
|
+
"font-family",
|
|
46
|
+
"line-height",
|
|
47
|
+
"text-align",
|
|
48
|
+
"text-decoration",
|
|
49
|
+
"white-space",
|
|
50
|
+
"vertical-align",
|
|
51
|
+
"width",
|
|
52
|
+
"max-width",
|
|
53
|
+
"padding-top",
|
|
54
|
+
"padding-right",
|
|
55
|
+
"padding-bottom",
|
|
56
|
+
"padding-left",
|
|
57
|
+
"margin-top",
|
|
58
|
+
"margin-right",
|
|
59
|
+
"margin-bottom",
|
|
60
|
+
"margin-left",
|
|
61
|
+
"border-top",
|
|
62
|
+
"border-right",
|
|
63
|
+
"border-bottom",
|
|
64
|
+
"border-left",
|
|
65
|
+
"border-radius",
|
|
66
|
+
"border-collapse",
|
|
67
|
+
"flex-direction",
|
|
68
|
+
"flex-wrap",
|
|
69
|
+
"flex-grow",
|
|
70
|
+
"flex-shrink",
|
|
71
|
+
"flex-basis",
|
|
72
|
+
"align-items",
|
|
73
|
+
"justify-content",
|
|
74
|
+
"gap",
|
|
75
|
+
"grid-template-columns",
|
|
76
|
+
"grid-template-rows",
|
|
77
|
+
"fill",
|
|
78
|
+
"stroke",
|
|
79
|
+
"stroke-width"
|
|
80
|
+
];
|
|
81
|
+
for (let index = 0; index < originals.length; index++) {
|
|
82
|
+
signal.throwIfAborted();
|
|
83
|
+
const original = originals[index];
|
|
84
|
+
const clone = copies[index];
|
|
85
|
+
if (!(clone instanceof HTMLElement || clone instanceof SVGElement))
|
|
86
|
+
continue;
|
|
87
|
+
const computed = getComputedStyle(original);
|
|
88
|
+
clone.removeAttribute("class");
|
|
89
|
+
clone.removeAttribute("id");
|
|
90
|
+
clone.removeAttribute("style");
|
|
91
|
+
for (const property of properties)
|
|
92
|
+
clone.style.setProperty(property, computed.getPropertyValue(property));
|
|
93
|
+
if (original instanceof HTMLElement && original.children.length === 0 && original.textContent?.trim() && !original.style.width)
|
|
94
|
+
clone.style.setProperty("width", "auto");
|
|
95
|
+
clone.style.setProperty("overflow", "visible");
|
|
96
|
+
clone.style.setProperty("max-width", "100%");
|
|
97
|
+
if (!original.textContent?.trim() && original.children.length === 0)
|
|
98
|
+
clone.style.setProperty("height", computed.height);
|
|
99
|
+
if (computed.whiteSpace === "nowrap")
|
|
100
|
+
clone.style.setProperty("white-space", "normal");
|
|
101
|
+
for (const attribute of [...clone.attributes]) {
|
|
102
|
+
if (attribute.name.startsWith("on") || ["tabindex", "autofocus", "contenteditable"].includes(attribute.name))
|
|
103
|
+
clone.removeAttribute(attribute.name);
|
|
104
|
+
}
|
|
105
|
+
if (original instanceof HTMLImageElement) {
|
|
106
|
+
if (includeImages) {
|
|
107
|
+
const response = await fetch(original.currentSrc || original.src, { signal: resourceSignal });
|
|
108
|
+
if (!response.ok)
|
|
109
|
+
throw new Error("N\xE3o foi poss\xEDvel carregar uma imagem da conversa.");
|
|
110
|
+
const blob = await response.blob();
|
|
111
|
+
if (blob.size > 8 * 1024 * 1024)
|
|
112
|
+
throw new Error("Uma imagem excede o tamanho permitido para exporta\xE7\xE3o.");
|
|
113
|
+
const imageUrl = URL.createObjectURL(blob);
|
|
114
|
+
try {
|
|
115
|
+
const image = new Image();
|
|
116
|
+
image.src = imageUrl;
|
|
117
|
+
await waitForResource(image.decode());
|
|
118
|
+
const canvas = document.createElement("canvas");
|
|
119
|
+
canvas.width = image.naturalWidth;
|
|
120
|
+
canvas.height = image.naturalHeight;
|
|
121
|
+
if (canvas.width * canvas.height > 16e6)
|
|
122
|
+
throw new Error("Uma imagem excede a resolu\xE7\xE3o permitida para exporta\xE7\xE3o.");
|
|
123
|
+
const context = canvas.getContext("2d");
|
|
124
|
+
if (!context)
|
|
125
|
+
throw new Error("N\xE3o foi poss\xEDvel preparar as imagens para exporta\xE7\xE3o.");
|
|
126
|
+
context.drawImage(image, 0, 0);
|
|
127
|
+
clone.setAttribute("src", canvas.toDataURL("image/png"));
|
|
128
|
+
} finally {
|
|
129
|
+
URL.revokeObjectURL(imageUrl);
|
|
130
|
+
}
|
|
131
|
+
} else {
|
|
132
|
+
clone.removeAttribute("src");
|
|
133
|
+
}
|
|
134
|
+
clone.removeAttribute("srcset");
|
|
135
|
+
clone.removeAttribute("loading");
|
|
136
|
+
}
|
|
137
|
+
if (original instanceof HTMLInputElement || original instanceof HTMLTextAreaElement || original instanceof HTMLSelectElement) {
|
|
138
|
+
const span = document.createElement("span");
|
|
139
|
+
span.textContent = original instanceof HTMLInputElement && ["checkbox", "radio"].includes(original.type) ? original.checked ? "\u2611" : "\u2610" : original.value || original.getAttribute("placeholder") || "";
|
|
140
|
+
span.setAttribute("style", clone.getAttribute("style") ?? "");
|
|
141
|
+
clone.replaceWith(span);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
for (const element of copy.querySelectorAll('script, style, iframe, object, embed, audio, video, [aria-hidden="true"]'))
|
|
145
|
+
element.remove();
|
|
146
|
+
for (const button of copy.querySelectorAll("button")) {
|
|
147
|
+
const span = document.createElement("span");
|
|
148
|
+
span.setAttribute("style", button.getAttribute("style") ?? "");
|
|
149
|
+
span.append(...button.childNodes);
|
|
150
|
+
button.replaceWith(span);
|
|
151
|
+
}
|
|
152
|
+
signal.throwIfAborted();
|
|
153
|
+
return copy.outerHTML;
|
|
154
|
+
}
|
|
@@ -1,2 +1,6 @@
|
|
|
1
1
|
import type { ConversationWithMessages } from '../types/schemas/chat/conversations.js';
|
|
2
2
|
export declare function exportConversationToMarkdown(conversation: ConversationWithMessages): void;
|
|
3
|
+
export type ConversationExportFormat = 'md' | 'txt' | 'pdf';
|
|
4
|
+
export declare function conversationExportFilename(title: string, format: ConversationExportFormat): string;
|
|
5
|
+
export declare function renderedConversationToText(html: string): string;
|
|
6
|
+
export declare function downloadConversationFile(blob: Blob, filename: string): void;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { Parser } from "htmlparser2";
|
|
1
2
|
export function exportConversationToMarkdown(conversation) {
|
|
2
3
|
const formatDate = (date) => {
|
|
3
4
|
const d = new Date(date);
|
|
@@ -30,14 +31,78 @@ export function exportConversationToMarkdown(conversation) {
|
|
|
30
31
|
lines.push("");
|
|
31
32
|
}
|
|
32
33
|
const content = lines.join("\n");
|
|
33
|
-
const sanitizedTitle = conversation.title.replace(/[<>:"/\\|?*]/g, "_");
|
|
34
34
|
const blob = new Blob([content], { type: "text/markdown;charset=utf-8" });
|
|
35
|
+
downloadConversationFile(blob, conversationExportFilename(conversation.title, "md"));
|
|
36
|
+
}
|
|
37
|
+
export function conversationExportFilename(title, format) {
|
|
38
|
+
return `${title.trim().replace(/[<>:"/\\|?*\p{Cc}]/gu, "_").slice(0, 150) || "Conversa"}.${format}`;
|
|
39
|
+
}
|
|
40
|
+
export function renderedConversationToText(html) {
|
|
41
|
+
const blocks = /* @__PURE__ */ new Set(["h1", "h2", "h3", "h4", "h5", "h6", "p", "div", "section", "article", "header", "footer", "table", "ul", "ol", "pre", "blockquote", "figure"]);
|
|
42
|
+
const ignored = /* @__PURE__ */ new Set(["script", "style", "svg"]);
|
|
43
|
+
const links = [];
|
|
44
|
+
let text = "";
|
|
45
|
+
let ignoredDepth = 0;
|
|
46
|
+
const whitespace = [];
|
|
47
|
+
let cells = 0;
|
|
48
|
+
const parser = new Parser({
|
|
49
|
+
onopentag(name, attributes) {
|
|
50
|
+
if (ignoredDepth || ignored.has(name)) {
|
|
51
|
+
ignoredDepth++;
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
if (blocks.has(name))
|
|
55
|
+
text += "\n\n";
|
|
56
|
+
const style = attributes.style?.match(/(?:^|;)\s*white-space\s*:\s*([\w-]+)/i)?.[1]?.toLowerCase();
|
|
57
|
+
whitespace.push(style ?? (name === "pre" ? "pre" : whitespace.at(-1) ?? "normal"));
|
|
58
|
+
if (name === "tr") {
|
|
59
|
+
cells = 0;
|
|
60
|
+
text += "\n";
|
|
61
|
+
}
|
|
62
|
+
if (name === "td" || name === "th") {
|
|
63
|
+
if (cells++)
|
|
64
|
+
text += " ";
|
|
65
|
+
}
|
|
66
|
+
if (name === "li")
|
|
67
|
+
text += "\n- ";
|
|
68
|
+
if (name === "br" || name === "hr")
|
|
69
|
+
text += "\n";
|
|
70
|
+
if (name === "img" && attributes.alt)
|
|
71
|
+
text += `[${attributes.alt}]`;
|
|
72
|
+
if (name === "a")
|
|
73
|
+
links.push(attributes.href ?? "");
|
|
74
|
+
},
|
|
75
|
+
ontext(value) {
|
|
76
|
+
if (!ignoredDepth) {
|
|
77
|
+
const mode = whitespace.at(-1);
|
|
78
|
+
text += mode === "pre" || mode === "pre-wrap" || mode === "break-spaces" ? value : mode === "pre-line" ? value.replace(/[^\S\r\n]+/g, " ") : value.replace(/\s+/g, " ");
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
onclosetag(name) {
|
|
82
|
+
if (ignoredDepth) {
|
|
83
|
+
ignoredDepth--;
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
whitespace.pop();
|
|
87
|
+
if (name === "a") {
|
|
88
|
+
const href = links.pop();
|
|
89
|
+
if (href && /^(?:https?:|mailto:|vault:|cite:)/.test(href))
|
|
90
|
+
text += ` (${href})`;
|
|
91
|
+
}
|
|
92
|
+
if (blocks.has(name))
|
|
93
|
+
text += "\n\n";
|
|
94
|
+
}
|
|
95
|
+
}, { decodeEntities: true });
|
|
96
|
+
parser.end(html);
|
|
97
|
+
return text.replace(/[ \t]+\n/g, "\n").replace(/\n{3,}/g, "\n\n").trim();
|
|
98
|
+
}
|
|
99
|
+
export function downloadConversationFile(blob, filename) {
|
|
35
100
|
const url = URL.createObjectURL(blob);
|
|
36
101
|
const link = document.createElement("a");
|
|
37
102
|
link.href = url;
|
|
38
|
-
link.download =
|
|
103
|
+
link.download = filename;
|
|
39
104
|
document.body.appendChild(link);
|
|
40
105
|
link.click();
|
|
41
|
-
|
|
42
|
-
URL.revokeObjectURL(url);
|
|
106
|
+
link.remove();
|
|
107
|
+
setTimeout(() => URL.revokeObjectURL(url), 1e3);
|
|
43
108
|
}
|
|
@@ -15,6 +15,8 @@ import ChatMobileShell from "./mobile/chat-mobile-shell.vue";
|
|
|
15
15
|
import ChatSidebarToggle from "./chat-sidebar-toggle.vue";
|
|
16
16
|
import ChatDetailsPanel from "./chat-details-panel.vue";
|
|
17
17
|
import ChatMessageInput from "../../messages/components/chat-message-input.vue";
|
|
18
|
+
import { confirmPendingComposerSend, prepareComposerSend, reconcileComposerSend } from "../../messages/composer-pending-send";
|
|
19
|
+
import { composerDraftStorageKey, readComposerDraft, writeComposerDraft } from "../../messages/composer-draft";
|
|
18
20
|
import { normalizeConversationId } from "../../conversations/scope";
|
|
19
21
|
import { resolveChatFeatures } from "../feature-config";
|
|
20
22
|
import { resolveConversationCreator } from "../../conversations/creator";
|
|
@@ -196,14 +198,71 @@ const resolvedSuggestions = computed(
|
|
|
196
198
|
function applySuggestion(prompt) {
|
|
197
199
|
messageInputRef.value?.setDraft(prompt);
|
|
198
200
|
}
|
|
201
|
+
watch([isReady, messageInputRef, activeConversationId], ([ready, input]) => {
|
|
202
|
+
if (!ready || !input || activeConversationId.value)
|
|
203
|
+
return;
|
|
204
|
+
const draft = composerExtension?.takeInitialDraft?.();
|
|
205
|
+
if (!draft)
|
|
206
|
+
return;
|
|
207
|
+
if (draft.executionProfile)
|
|
208
|
+
selectExecutionProfile(draft.executionProfile);
|
|
209
|
+
input.setDraft(draft.content);
|
|
210
|
+
}, { flush: "post" });
|
|
199
211
|
const breakpoints = useBreakpoints({ mobile: CHAT_MOBILE_BREAKPOINT });
|
|
200
212
|
const isMobile = breakpoints.smaller("mobile");
|
|
201
213
|
const showTopbarSidebarToggle = computed(() => sidebarCollapsible.value && sidebarToggle.value === "topbar" && !props.hideSidebar && !isMobile.value && !props.compact);
|
|
202
214
|
const slots = useSlots();
|
|
203
215
|
const hasTopbarLeading = computed(() => showTopbarSidebarToggle.value || !!slots["topbar-leading"]);
|
|
204
216
|
const drawerOpen = ref(false);
|
|
217
|
+
function composerKey(conversationId) {
|
|
218
|
+
return composerDraftStorageKey(
|
|
219
|
+
{ userId: authUser.value?.id, workspaceId: activeOrganization.value?.id, telaAgentId: embedConfig?.telaAgentId.value, conversationScope: embedConfig?.conversationScope?.value },
|
|
220
|
+
conversationId
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
const draftStorageKey = computed(() => composerKey(activeConversationId.value));
|
|
224
|
+
let unconfirmedDraft = null;
|
|
225
|
+
function releaseUnconfirmedDraft(draft) {
|
|
226
|
+
if (readComposerDraft(draft.key) === draft.text)
|
|
227
|
+
writeComposerDraft(draft.key, "");
|
|
228
|
+
}
|
|
229
|
+
function confirmComposerDraft(send) {
|
|
230
|
+
confirmPendingComposerSend(composerKey(send.conversationId), send.messageId);
|
|
231
|
+
if (!unconfirmedDraft || unconfirmedDraft.content !== send.content)
|
|
232
|
+
return;
|
|
233
|
+
if (unconfirmedDraft.conversationId !== null && unconfirmedDraft.conversationId !== send.conversationId)
|
|
234
|
+
return;
|
|
235
|
+
releaseUnconfirmedDraft(unconfirmedDraft);
|
|
236
|
+
unconfirmedDraft = null;
|
|
237
|
+
}
|
|
238
|
+
function prepareComposerRequest(send) {
|
|
239
|
+
if (!unconfirmedDraft || unconfirmedDraft.content !== send.body.content)
|
|
240
|
+
return send.body.messageId;
|
|
241
|
+
return prepareComposerSend(unconfirmedDraft.key, unconfirmedDraft.text, send.body);
|
|
242
|
+
}
|
|
243
|
+
function moveUnconfirmedDraft(conversationId) {
|
|
244
|
+
if (!unconfirmedDraft || unconfirmedDraft.conversationId !== null)
|
|
245
|
+
return;
|
|
246
|
+
const key = draftStorageKey.value;
|
|
247
|
+
if (key === unconfirmedDraft.key || !unconfirmedDraft.text) {
|
|
248
|
+
unconfirmedDraft = { ...unconfirmedDraft, key, conversationId };
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
if (!writeComposerDraft(key, unconfirmedDraft.text)) {
|
|
252
|
+
unconfirmedDraft = { ...unconfirmedDraft, conversationId };
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
releaseUnconfirmedDraft(unconfirmedDraft);
|
|
256
|
+
unconfirmedDraft = { ...unconfirmedDraft, key, conversationId };
|
|
257
|
+
}
|
|
205
258
|
function restoreQueuedSend(send) {
|
|
206
|
-
messageInputRef.value
|
|
259
|
+
const input = messageInputRef.value;
|
|
260
|
+
if (!input)
|
|
261
|
+
return;
|
|
262
|
+
input.restoreDraft(send);
|
|
263
|
+
if (unconfirmedDraft && unconfirmedDraft.key !== draftStorageKey.value && readComposerDraft(draftStorageKey.value))
|
|
264
|
+
releaseUnconfirmedDraft(unconfirmedDraft);
|
|
265
|
+
unconfirmedDraft = null;
|
|
207
266
|
}
|
|
208
267
|
const { isDragging, setupDropZone } = useFileDrop({
|
|
209
268
|
onDrop: (files) => {
|
|
@@ -244,7 +303,10 @@ const {
|
|
|
244
303
|
} = createChat({
|
|
245
304
|
outboundQueueEnabled: computed(() => resolvedFeatures.value.chatOutboundQueue),
|
|
246
305
|
onQueuedSendCancelled: restoreQueuedSend,
|
|
247
|
-
onQueuedSendFailed: restoreQueuedSend
|
|
306
|
+
onQueuedSendFailed: restoreQueuedSend,
|
|
307
|
+
onSendConfirmed: confirmComposerDraft,
|
|
308
|
+
onSendPrepared: prepareComposerRequest,
|
|
309
|
+
onMessagesReceived: ({ conversationId, messages: messages2 }) => reconcileComposerSend(composerKey(conversationId), messages2)
|
|
248
310
|
});
|
|
249
311
|
const agentWake = harnessAdapter?.requiresSandboxWake === false ? null : useAgentWake();
|
|
250
312
|
watch(chatError, (message) => {
|
|
@@ -544,6 +606,12 @@ function claimableWakeSessionId() {
|
|
|
544
606
|
async function handleSend(content, files, agentInputs) {
|
|
545
607
|
const prepared = composerExtension?.prepareSend?.(content);
|
|
546
608
|
const outbound = prepared?.content ?? content;
|
|
609
|
+
unconfirmedDraft = {
|
|
610
|
+
key: draftStorageKey.value,
|
|
611
|
+
conversationId: activeConversationId.value,
|
|
612
|
+
content: outbound,
|
|
613
|
+
text: readComposerDraft(draftStorageKey.value)
|
|
614
|
+
};
|
|
547
615
|
const resolvedAgentInputs = resolveTelaAgentInputs(agentInputs, embedConfig?.telaAgentInputs?.value);
|
|
548
616
|
const sent = await dispatchMessage(outbound, files, resolvedAgentInputs);
|
|
549
617
|
if (sent)
|
|
@@ -555,8 +623,13 @@ async function dispatchMessage(outbound, files, resolvedAgentInputs) {
|
|
|
555
623
|
const executionSettings = { model: selectedModel.value, reasoningEffort: selectedReasoningEffort.value };
|
|
556
624
|
if (props.sendBlocked)
|
|
557
625
|
return false;
|
|
626
|
+
const requestedConversationId = activeConversationId.value;
|
|
627
|
+
if (requestedConversationId && currentConversation.value?.id !== requestedConversationId)
|
|
628
|
+
return null;
|
|
558
629
|
let claimedWakeSessionId;
|
|
559
630
|
const sent = await sendMessage(outbound, files.length > 0 ? files : void 0, resolvedAgentInputs, async (signal) => {
|
|
631
|
+
if (requestedConversationId && (activeConversationId.value !== requestedConversationId || currentConversation.value?.id !== requestedConversationId))
|
|
632
|
+
throw new Error("A conversa mudou. Abra o hist\xF3rico novamente.");
|
|
560
633
|
if (!activeConversationId.value || !currentConversation.value) {
|
|
561
634
|
const conversation2 = await createConversation({
|
|
562
635
|
...executionSettings,
|
|
@@ -569,14 +642,20 @@ async function dispatchMessage(outbound, files, resolvedAgentInputs) {
|
|
|
569
642
|
const grouped = groupId && await moveConversationToGroup(conversation2.id, groupId);
|
|
570
643
|
adoptConversation(grouped ? { ...conversation2, groupId } : conversation2);
|
|
571
644
|
updateConversationId(conversation2.id);
|
|
645
|
+
moveUnconfirmedDraft(conversation2.id);
|
|
572
646
|
}
|
|
573
647
|
const conversation = currentConversation.value;
|
|
574
|
-
if (!conversation)
|
|
648
|
+
if (!conversation || requestedConversationId && (activeConversationId.value !== requestedConversationId || conversation.id !== requestedConversationId))
|
|
575
649
|
throw new Error("Erro ao preparar conversa");
|
|
576
650
|
if (!agentWake || chatApi.conversationV2Enabled)
|
|
577
651
|
return { executionSettings };
|
|
652
|
+
const assertConversationStillOpen = () => {
|
|
653
|
+
if (activeConversationId.value !== conversation.id || currentConversation.value?.id !== conversation.id)
|
|
654
|
+
throw new Error("A conversa mudou. Abra o hist\xF3rico novamente.");
|
|
655
|
+
};
|
|
578
656
|
if (conversation.threadSessionId && resolvedFeatures.value.chatOutboundQueue) {
|
|
579
657
|
const wake = await agentWake.ensureReady(conversation.threadSessionId, executionSettings.model);
|
|
658
|
+
assertConversationStillOpen();
|
|
580
659
|
if (wake.status !== "ready") {
|
|
581
660
|
if (wake.wokenSandbox && !wake.wokenSandbox.rewakeable)
|
|
582
661
|
return { defer: true, executionSettings };
|
|
@@ -589,6 +668,7 @@ async function dispatchMessage(outbound, files, resolvedAgentInputs) {
|
|
|
589
668
|
if (agentWake.state.value === "idle")
|
|
590
669
|
await agentWake.wake(void 0, executionSettings.model);
|
|
591
670
|
}
|
|
671
|
+
assertConversationStillOpen();
|
|
592
672
|
claimedWakeSessionId = claimableWakeSessionId();
|
|
593
673
|
if (!conversation.threadSessionId && !claimedWakeSessionId)
|
|
594
674
|
throw new Error("N\xE3o foi poss\xEDvel acordar o agente. Tente novamente.");
|
|
@@ -836,7 +916,7 @@ async function handleDelete() {
|
|
|
836
916
|
icon="i-ph-arrow-clockwise"
|
|
837
917
|
@click="reconnect"
|
|
838
918
|
>
|
|
839
|
-
Reconectar
|
|
919
|
+
{{ outboundOccupied ? "Reenviar" : "Reconectar" }}
|
|
840
920
|
</TelaButton>
|
|
841
921
|
</div>
|
|
842
922
|
<!-- Sidebar -->
|
|
@@ -943,6 +1023,7 @@ async function handleDelete() {
|
|
|
943
1023
|
:generating-title="generatingTitle"
|
|
944
1024
|
:has-messages="hasMessages"
|
|
945
1025
|
:has-pending-message="composerLocked"
|
|
1026
|
+
:draft-storage-key="draftStorageKey"
|
|
946
1027
|
:hide-settings="hideSettings"
|
|
947
1028
|
:hide-sidebar="hideSidebar"
|
|
948
1029
|
:is-editing="isEditing"
|
|
@@ -1030,7 +1111,8 @@ async function handleDelete() {
|
|
|
1030
1111
|
ref="messageInputRef"
|
|
1031
1112
|
v-model:selected-model="selectedModel"
|
|
1032
1113
|
v-model:selected-reasoning-effort="selectedReasoningEffort"
|
|
1033
|
-
:
|
|
1114
|
+
:draft-storage-key="draftStorageKey"
|
|
1115
|
+
:loading="loading || composerLocked"
|
|
1034
1116
|
:disabled="composerDisabled"
|
|
1035
1117
|
:send-blocked="sendBlocked"
|
|
1036
1118
|
:creator-name="currentUser?.name ?? void 0"
|
|
@@ -1086,8 +1168,9 @@ async function handleDelete() {
|
|
|
1086
1168
|
ref="messageInputRef"
|
|
1087
1169
|
v-model:selected-model="selectedModel"
|
|
1088
1170
|
v-model:selected-reasoning-effort="selectedReasoningEffort"
|
|
1171
|
+
:draft-storage-key="draftStorageKey"
|
|
1089
1172
|
class="!p-0"
|
|
1090
|
-
:loading="composerLocked"
|
|
1173
|
+
:loading="loading || composerLocked"
|
|
1091
1174
|
:disabled="composerDisabled"
|
|
1092
1175
|
:send-blocked="sendBlocked"
|
|
1093
1176
|
:creator-name="currentUser?.name ?? void 0"
|
|
@@ -1241,7 +1324,8 @@ async function handleDelete() {
|
|
|
1241
1324
|
ref="messageInputRef"
|
|
1242
1325
|
v-model:selected-model="selectedModel"
|
|
1243
1326
|
v-model:selected-reasoning-effort="selectedReasoningEffort"
|
|
1244
|
-
:
|
|
1327
|
+
:draft-storage-key="draftStorageKey"
|
|
1328
|
+
:loading="loading || composerLocked"
|
|
1245
1329
|
:disabled="composerDisabled"
|
|
1246
1330
|
:send-blocked="sendBlocked"
|
|
1247
1331
|
:creator-name="currentUser?.name ?? void 0"
|
|
@@ -1300,8 +1384,9 @@ async function handleDelete() {
|
|
|
1300
1384
|
ref="messageInputRef"
|
|
1301
1385
|
v-model:selected-model="selectedModel"
|
|
1302
1386
|
v-model:selected-reasoning-effort="selectedReasoningEffort"
|
|
1387
|
+
:draft-storage-key="draftStorageKey"
|
|
1303
1388
|
class="!p-0"
|
|
1304
|
-
:loading="composerLocked"
|
|
1389
|
+
:loading="loading || composerLocked"
|
|
1305
1390
|
:disabled="composerDisabled"
|
|
1306
1391
|
:send-blocked="sendBlocked"
|
|
1307
1392
|
:creator-name="currentUser?.name ?? void 0"
|
|
@@ -10,12 +10,12 @@ type __VLS_Props = {
|
|
|
10
10
|
userImage?: string;
|
|
11
11
|
};
|
|
12
12
|
declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {} & {
|
|
13
|
-
close: () => any;
|
|
14
13
|
select: (id: string) => any;
|
|
14
|
+
close: () => any;
|
|
15
15
|
newConvo: (groupId?: string | null | undefined) => any;
|
|
16
16
|
}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
|
|
17
|
-
onClose?: (() => any) | undefined;
|
|
18
17
|
onSelect?: ((id: string) => any) | undefined;
|
|
18
|
+
onClose?: (() => any) | undefined;
|
|
19
19
|
onNewConvo?: ((groupId?: string | null | undefined) => any) | undefined;
|
|
20
20
|
}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
21
21
|
declare const _default: typeof __VLS_export;
|
|
@@ -10,12 +10,12 @@ type __VLS_Props = {
|
|
|
10
10
|
userImage?: string;
|
|
11
11
|
};
|
|
12
12
|
declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {} & {
|
|
13
|
-
close: () => any;
|
|
14
13
|
select: (id: string) => any;
|
|
14
|
+
close: () => any;
|
|
15
15
|
newConvo: (groupId?: string | null | undefined) => any;
|
|
16
16
|
}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
|
|
17
|
-
onClose?: (() => any) | undefined;
|
|
18
17
|
onSelect?: ((id: string) => any) | undefined;
|
|
18
|
+
onClose?: (() => any) | undefined;
|
|
19
19
|
onNewConvo?: ((groupId?: string | null | undefined) => any) | undefined;
|
|
20
20
|
}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
21
21
|
declare const _default: typeof __VLS_export;
|
|
@@ -22,6 +22,7 @@ type __VLS_Props = {
|
|
|
22
22
|
conversationsLoading?: boolean;
|
|
23
23
|
conversationFiles: ConversationFile[];
|
|
24
24
|
drawerOpen: boolean;
|
|
25
|
+
draftStorageKey?: string | null;
|
|
25
26
|
editTitle: string;
|
|
26
27
|
filesCount: number;
|
|
27
28
|
generatingTitle?: boolean;
|
|
@@ -64,15 +65,15 @@ declare const __VLS_base: import("vue").DefineComponent<__VLS_PublicProps, {}, {
|
|
|
64
65
|
"update:selectedReasoningEffort": (value: any) => any;
|
|
65
66
|
} & {
|
|
66
67
|
delete: () => any;
|
|
68
|
+
cancel: () => any;
|
|
67
69
|
rename: () => any;
|
|
68
70
|
duplicate: () => any;
|
|
69
71
|
export: () => any;
|
|
70
72
|
copyLink: () => any;
|
|
71
|
-
cancel: () => any;
|
|
72
73
|
debug: () => any;
|
|
73
74
|
selectConversation: (id: string) => any;
|
|
74
|
-
settings: () => any;
|
|
75
75
|
send: (content: string, files: FileUploadInput[], agentInputs?: TelaAgentExecutionInput[] | undefined) => any;
|
|
76
|
+
settings: () => any;
|
|
76
77
|
selectExecutionProfile: (profile: ChatExecutionProfile) => any;
|
|
77
78
|
tabChange: (tab: TopbarTab) => any;
|
|
78
79
|
newConversation: (groupId?: string | null | undefined) => any;
|
|
@@ -86,15 +87,15 @@ declare const __VLS_base: import("vue").DefineComponent<__VLS_PublicProps, {}, {
|
|
|
86
87
|
"update:workspaceSheetOpen": (value: boolean) => any;
|
|
87
88
|
}, string, import("vue").PublicProps, Readonly<__VLS_PublicProps> & Readonly<{
|
|
88
89
|
onDelete?: (() => any) | undefined;
|
|
90
|
+
onCancel?: (() => any) | undefined;
|
|
89
91
|
onRename?: (() => any) | undefined;
|
|
90
92
|
onDuplicate?: (() => any) | undefined;
|
|
91
93
|
onExport?: (() => any) | undefined;
|
|
92
94
|
onCopyLink?: (() => any) | undefined;
|
|
93
|
-
onCancel?: (() => any) | undefined;
|
|
94
95
|
onDebug?: (() => any) | undefined;
|
|
95
96
|
onSelectConversation?: ((id: string) => any) | undefined;
|
|
96
|
-
onSettings?: (() => any) | undefined;
|
|
97
97
|
onSend?: ((content: string, files: FileUploadInput[], agentInputs?: TelaAgentExecutionInput[] | undefined) => any) | undefined;
|
|
98
|
+
onSettings?: (() => any) | undefined;
|
|
98
99
|
onSelectExecutionProfile?: ((profile: ChatExecutionProfile) => any) | undefined;
|
|
99
100
|
"onUpdate:selectedModel"?: ((value: string | null) => any) | undefined;
|
|
100
101
|
"onUpdate:selectedReasoningEffort"?: ((value: any) => any) | undefined;
|
|
@@ -27,6 +27,7 @@ const props = defineProps({
|
|
|
27
27
|
conversationsLoading: { type: Boolean, required: false },
|
|
28
28
|
conversationFiles: { type: Array, required: true },
|
|
29
29
|
drawerOpen: { type: Boolean, required: true },
|
|
30
|
+
draftStorageKey: { type: [String, null], required: false },
|
|
30
31
|
editTitle: { type: String, required: true },
|
|
31
32
|
filesCount: { type: Number, required: true },
|
|
32
33
|
generatingTitle: { type: Boolean, required: false },
|
|
@@ -235,6 +236,7 @@ function startNewConversationFromOutdatedSettings() {
|
|
|
235
236
|
v-model:selected-model="selectedModel"
|
|
236
237
|
v-model:selected-reasoning-effort="selectedReasoningEffort"
|
|
237
238
|
:loading="hasPendingMessage"
|
|
239
|
+
:draft-storage-key="draftStorageKey"
|
|
238
240
|
:creator-name="currentUser?.name ?? void 0"
|
|
239
241
|
:creator-email="currentUser?.email ?? void 0"
|
|
240
242
|
:creator-image="currentUser?.image ?? void 0"
|
|
@@ -22,6 +22,7 @@ type __VLS_Props = {
|
|
|
22
22
|
conversationsLoading?: boolean;
|
|
23
23
|
conversationFiles: ConversationFile[];
|
|
24
24
|
drawerOpen: boolean;
|
|
25
|
+
draftStorageKey?: string | null;
|
|
25
26
|
editTitle: string;
|
|
26
27
|
filesCount: number;
|
|
27
28
|
generatingTitle?: boolean;
|
|
@@ -64,15 +65,15 @@ declare const __VLS_base: import("vue").DefineComponent<__VLS_PublicProps, {}, {
|
|
|
64
65
|
"update:selectedReasoningEffort": (value: any) => any;
|
|
65
66
|
} & {
|
|
66
67
|
delete: () => any;
|
|
68
|
+
cancel: () => any;
|
|
67
69
|
rename: () => any;
|
|
68
70
|
duplicate: () => any;
|
|
69
71
|
export: () => any;
|
|
70
72
|
copyLink: () => any;
|
|
71
|
-
cancel: () => any;
|
|
72
73
|
debug: () => any;
|
|
73
74
|
selectConversation: (id: string) => any;
|
|
74
|
-
settings: () => any;
|
|
75
75
|
send: (content: string, files: FileUploadInput[], agentInputs?: TelaAgentExecutionInput[] | undefined) => any;
|
|
76
|
+
settings: () => any;
|
|
76
77
|
selectExecutionProfile: (profile: ChatExecutionProfile) => any;
|
|
77
78
|
tabChange: (tab: TopbarTab) => any;
|
|
78
79
|
newConversation: (groupId?: string | null | undefined) => any;
|
|
@@ -86,15 +87,15 @@ declare const __VLS_base: import("vue").DefineComponent<__VLS_PublicProps, {}, {
|
|
|
86
87
|
"update:workspaceSheetOpen": (value: boolean) => any;
|
|
87
88
|
}, string, import("vue").PublicProps, Readonly<__VLS_PublicProps> & Readonly<{
|
|
88
89
|
onDelete?: (() => any) | undefined;
|
|
90
|
+
onCancel?: (() => any) | undefined;
|
|
89
91
|
onRename?: (() => any) | undefined;
|
|
90
92
|
onDuplicate?: (() => any) | undefined;
|
|
91
93
|
onExport?: (() => any) | undefined;
|
|
92
94
|
onCopyLink?: (() => any) | undefined;
|
|
93
|
-
onCancel?: (() => any) | undefined;
|
|
94
95
|
onDebug?: (() => any) | undefined;
|
|
95
96
|
onSelectConversation?: ((id: string) => any) | undefined;
|
|
96
|
-
onSettings?: (() => any) | undefined;
|
|
97
97
|
onSend?: ((content: string, files: FileUploadInput[], agentInputs?: TelaAgentExecutionInput[] | undefined) => any) | undefined;
|
|
98
|
+
onSettings?: (() => any) | undefined;
|
|
98
99
|
onSelectExecutionProfile?: ((profile: ChatExecutionProfile) => any) | undefined;
|
|
99
100
|
"onUpdate:selectedModel"?: ((value: string | null) => any) | undefined;
|
|
100
101
|
"onUpdate:selectedReasoningEffort"?: ((value: any) => any) | undefined;
|
|
@@ -11,6 +11,7 @@ export type ChatExecutionControls = {
|
|
|
11
11
|
profiles: {
|
|
12
12
|
default: ChatExecutionProfile;
|
|
13
13
|
deep: ChatExecutionProfile;
|
|
14
|
+
intensive?: ChatExecutionProfile;
|
|
14
15
|
};
|
|
15
16
|
} | {
|
|
16
17
|
mode: 'advanced';
|
|
@@ -23,4 +24,4 @@ export declare function resolveChatExecutionMode(profiles: Extract<ChatExecution
|
|
|
23
24
|
}>['profiles'], selection: {
|
|
24
25
|
model: string | null;
|
|
25
26
|
reasoningEffort?: ChatReasoningEffort | null;
|
|
26
|
-
}): 'default' | 'deep' | 'custom';
|
|
27
|
+
}): 'default' | 'deep' | 'intensive' | 'custom';
|
|
@@ -9,9 +9,9 @@ export function resolveChatExecutionControls(controls, features) {
|
|
|
9
9
|
};
|
|
10
10
|
}
|
|
11
11
|
export function resolveChatExecutionMode(profiles, selection) {
|
|
12
|
-
for (const mode of ["default", "deep"]) {
|
|
12
|
+
for (const mode of ["default", "deep", "intensive"]) {
|
|
13
13
|
const profile = profiles[mode];
|
|
14
|
-
if (profile.model === selection.model && profile.reasoningEffort === (selection.reasoningEffort ?? null))
|
|
14
|
+
if (profile && profile.model === selection.model && profile.reasoningEffort === (selection.reasoningEffort ?? null))
|
|
15
15
|
return mode;
|
|
16
16
|
}
|
|
17
17
|
return "custom";
|
|
@@ -7,15 +7,19 @@ const props = defineProps({
|
|
|
7
7
|
disabled: { type: Boolean, required: false }
|
|
8
8
|
});
|
|
9
9
|
const emit = defineEmits(["select"]);
|
|
10
|
-
const options = [
|
|
10
|
+
const options = computed(() => [
|
|
11
11
|
{ value: "default", label: "Padr\xE3o", description: "Para as tarefas do dia a dia." },
|
|
12
|
-
{ value: "deep", label: "
|
|
13
|
-
]
|
|
12
|
+
{ value: "deep", label: "Aprofundado", description: "Dedica mais racioc\xEDnio a tarefas complexas. Pode demorar mais." },
|
|
13
|
+
...props.profiles.intensive ? [{ value: "intensive", label: "Intensivo", description: "Maior esfor\xE7o de racioc\xEDnio para quest\xF5es complexas." }] : []
|
|
14
|
+
]);
|
|
14
15
|
const mode = computed(() => resolveChatExecutionMode(props.profiles, props));
|
|
15
|
-
const label = computed(() => mode.value === "custom" ? "Personalizado" : options.find((option) => option.value === mode.value).label);
|
|
16
|
+
const label = computed(() => mode.value === "custom" ? "Personalizado" : options.value.find((option) => option.value === mode.value).label);
|
|
16
17
|
function select(value) {
|
|
17
|
-
if (
|
|
18
|
-
|
|
18
|
+
if (props.disabled || value !== "default" && value !== "deep" && value !== "intensive")
|
|
19
|
+
return;
|
|
20
|
+
const profile = props.profiles[value];
|
|
21
|
+
if (profile)
|
|
22
|
+
emit("select", { ...profile });
|
|
19
23
|
}
|
|
20
24
|
</script>
|
|
21
25
|
|
|
@@ -338,6 +338,13 @@ const attachedFiles = computed(() => {
|
|
|
338
338
|
<span v-if="showTtft && formattedTtft" flex items-center gap-4px>
|
|
339
339
|
{{ formattedTtft }}
|
|
340
340
|
</span>
|
|
341
|
+
<TelaCopyButton
|
|
342
|
+
:content="message.content"
|
|
343
|
+
variant="ghost"
|
|
344
|
+
size="sm"
|
|
345
|
+
aria-label="Copiar mensagem"
|
|
346
|
+
class="text-icon-tertiary hover:text-primary [&_.i-ph-copy]:!opacity-100"
|
|
347
|
+
/>
|
|
341
348
|
<ChatMessageFeedback v-if="hasMessageFeedback" :message="message" />
|
|
342
349
|
<ChatMessageActions
|
|
343
350
|
v-if="message.role === 'assistant' && message.status === 'completed' && !streamActive"
|