@meistrari/chat-nuxt 4.6.0-rc.1 → 4.6.0-rc.11
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/dist/module.json +1 -1
- package/dist/runtime/conversations/components/chat-conversation-item.d.vue.ts +1 -0
- package/dist/runtime/conversations/components/chat-conversation-item.vue +3 -2
- package/dist/runtime/conversations/components/chat-conversation-item.vue.d.ts +1 -0
- package/dist/runtime/conversations/components/chat-conversation-list.d.vue.ts +4 -1
- package/dist/runtime/conversations/components/chat-conversation-list.vue +1 -1
- package/dist/runtime/conversations/components/chat-conversation-list.vue.d.ts +4 -1
- package/dist/runtime/conversations/composables/conversation-item.d.ts +1 -0
- package/dist/runtime/conversations/composables/conversation-list.d.ts +5 -1
- package/dist/runtime/conversations/composables/conversation-list.js +3 -1
- package/dist/runtime/embed/components/chat-embed-inner.vue +2 -2
- package/dist/runtime/embed/components/chat-embed.vue +1 -0
- package/dist/runtime/embed/components/meistrari-chat-embed.vue +2 -1
- package/dist/runtime/embed/components/mobile/chat-mobile-shell.d.vue.ts +2 -2
- package/dist/runtime/embed/components/mobile/chat-mobile-shell.vue.d.ts +2 -2
- package/dist/runtime/embed/types.d.ts +2 -0
- package/dist/runtime/lightweight/messages/composables/use-direct-stream.d.ts +4 -4
- package/dist/runtime/lightweight/messages/composables/use-direct-stream.js +10 -3
- package/dist/runtime/messages/components/chat-message-actions.d.vue.ts +2 -0
- package/dist/runtime/messages/components/chat-message-actions.vue +27 -14
- package/dist/runtime/messages/components/chat-message-actions.vue.d.ts +2 -0
- package/dist/runtime/messages/components/chat-message-bubble.vue +3 -1
- package/dist/runtime/messages/components/chat-message-list.vue +14 -0
- package/dist/runtime/messages/components/reasoning/chat-reasoning-message.vue +23 -7
- package/dist/runtime/messages/components/reasoning/chat-reasoning-thought.vue +3 -0
- package/dist/runtime/messages/composables/chat.js +7 -28
- package/dist/runtime/messages/composables/current-day.d.ts +1 -0
- package/dist/runtime/messages/composables/current-day.js +17 -0
- package/dist/runtime/messages/debug/chat-debug-scenarios.js +105 -0
- package/dist/runtime/messages/message-reasoning.d.ts +0 -1
- package/dist/runtime/messages/message-reasoning.js +21 -10
- package/dist/runtime/messages/message-state.d.ts +11 -0
- package/dist/runtime/messages/message-state.js +37 -0
- package/dist/runtime/messages/message-timestamps.d.ts +5 -0
- package/dist/runtime/messages/message-timestamps.js +43 -0
- package/dist/runtime/messages/reasoning-step-key.js +2 -0
- package/dist/runtime/messages/reasoning.d.ts +5 -1
- package/dist/runtime/messages/tool-registry.js +17 -0
- package/dist/runtime/rendering/components/app-markdown-render.vue +2 -1
- package/dist/runtime/rendering/markdown/normalize-custom-tag-json.d.ts +1 -0
- package/dist/runtime/rendering/markdown/normalize-custom-tag-json.js +15 -0
- package/dist/runtime/server/utils/chat-api-proxy.js +4 -0
- package/dist/runtime/types/schemas/integrations/lightweight.js +2 -2
- package/dist/runtime/workspace/composables/workspace-members.d.ts +0 -1
- package/dist/runtime/workspace/composables/workspace-members.js +0 -4
- package/package.json +2 -2
package/dist/module.json
CHANGED
|
@@ -6,6 +6,7 @@ type __VLS_Props = {
|
|
|
6
6
|
draggable?: boolean;
|
|
7
7
|
itemClass?: HTMLAttributes['class'];
|
|
8
8
|
userImage?: string;
|
|
9
|
+
userName?: string;
|
|
9
10
|
selectConversation?: (id: string) => void | Promise<void>;
|
|
10
11
|
copyLink?: (id: string) => void | Promise<void>;
|
|
11
12
|
debug?: (id: string) => void;
|
|
@@ -6,6 +6,7 @@ const props = defineProps({
|
|
|
6
6
|
draggable: { type: Boolean, required: false },
|
|
7
7
|
itemClass: { type: [Boolean, null, String, Object, Array], required: false, skipCheck: true },
|
|
8
8
|
userImage: { type: String, required: false },
|
|
9
|
+
userName: { type: String, required: false },
|
|
9
10
|
selectConversation: { type: Function, required: false },
|
|
10
11
|
copyLink: { type: Function, required: false },
|
|
11
12
|
debug: { type: Function, required: false }
|
|
@@ -53,13 +54,13 @@ function setRenameInputRef(element) {
|
|
|
53
54
|
<div flex items-center justify-between h-full :class="props.itemClass">
|
|
54
55
|
<!-- User Avatar -->
|
|
55
56
|
<div
|
|
56
|
-
v-if="
|
|
57
|
+
v-if="userName"
|
|
57
58
|
translate-x--48px w-0 opacity-0 shrink-0 overflow-hidden
|
|
58
59
|
class="[transition:width_0.26s_cubic-bezier(0.22,1,0.36,1),transform_0.26s_cubic-bezier(0.22,1,0.36,1),opacity_0.2s] group-hover:w-22px group-hover:translate-x-0 group-hover:opacity-100"
|
|
59
60
|
>
|
|
60
61
|
<TelaAvatar
|
|
61
62
|
:image="userImage"
|
|
62
|
-
alt="
|
|
63
|
+
:alt="userName"
|
|
63
64
|
size="2xs"
|
|
64
65
|
class="rounded-full!"
|
|
65
66
|
/>
|
|
@@ -6,6 +6,7 @@ type __VLS_Props = {
|
|
|
6
6
|
draggable?: boolean;
|
|
7
7
|
itemClass?: HTMLAttributes['class'];
|
|
8
8
|
userImage?: string;
|
|
9
|
+
userName?: string;
|
|
9
10
|
selectConversation?: (id: string) => void | Promise<void>;
|
|
10
11
|
copyLink?: (id: string) => void | Promise<void>;
|
|
11
12
|
debug?: (id: string) => void;
|
|
@@ -3,7 +3,10 @@ import type { Conversation } from '../../types/schemas/chat/conversations.js';
|
|
|
3
3
|
type __VLS_Props = {
|
|
4
4
|
conversations: DeepReadonly<Conversation[]>;
|
|
5
5
|
currentId?: string;
|
|
6
|
-
|
|
6
|
+
getMember?: (userId: string) => {
|
|
7
|
+
name: string;
|
|
8
|
+
image?: string;
|
|
9
|
+
} | undefined;
|
|
7
10
|
selectConversation?: (id: string) => void | Promise<void>;
|
|
8
11
|
resetConversation?: () => void | Promise<void>;
|
|
9
12
|
startConversation?: (groupId?: string | null) => void | Promise<void>;
|
|
@@ -4,7 +4,7 @@ import { useConversationList } from "../composables/conversation-list";
|
|
|
4
4
|
const props = defineProps({
|
|
5
5
|
conversations: { type: null, required: true },
|
|
6
6
|
currentId: { type: String, required: false },
|
|
7
|
-
|
|
7
|
+
getMember: { type: Function, required: false },
|
|
8
8
|
selectConversation: { type: Function, required: false },
|
|
9
9
|
resetConversation: { type: Function, required: false },
|
|
10
10
|
startConversation: { type: Function, required: false },
|
|
@@ -3,7 +3,10 @@ import type { Conversation } from '../../types/schemas/chat/conversations.js';
|
|
|
3
3
|
type __VLS_Props = {
|
|
4
4
|
conversations: DeepReadonly<Conversation[]>;
|
|
5
5
|
currentId?: string;
|
|
6
|
-
|
|
6
|
+
getMember?: (userId: string) => {
|
|
7
|
+
name: string;
|
|
8
|
+
image?: string;
|
|
9
|
+
} | undefined;
|
|
7
10
|
selectConversation?: (id: string) => void | Promise<void>;
|
|
8
11
|
resetConversation?: () => void | Promise<void>;
|
|
9
12
|
startConversation?: (groupId?: string | null) => void | Promise<void>;
|
|
@@ -5,6 +5,7 @@ export type ConversationItemOptions = {
|
|
|
5
5
|
active: boolean;
|
|
6
6
|
draggable?: boolean;
|
|
7
7
|
userImage?: string;
|
|
8
|
+
userName?: string;
|
|
8
9
|
selectConversation?: (id: string) => void | Promise<void>;
|
|
9
10
|
copyLink?: (id: string) => void | Promise<void>;
|
|
10
11
|
debug?: (id: string) => void;
|
|
@@ -3,7 +3,10 @@ import type { Conversation } from '../../types/schemas/chat/conversations.js';
|
|
|
3
3
|
export type ConversationListOptions = {
|
|
4
4
|
conversations: DeepReadonly<Conversation[]>;
|
|
5
5
|
currentId?: string;
|
|
6
|
-
|
|
6
|
+
getMember?: (userId: string) => {
|
|
7
|
+
name: string;
|
|
8
|
+
image?: string;
|
|
9
|
+
} | undefined;
|
|
7
10
|
selectConversation?: (id: string) => void | Promise<void>;
|
|
8
11
|
resetConversation?: () => void | Promise<void>;
|
|
9
12
|
startConversation?: (groupId?: string | null) => void | Promise<void>;
|
|
@@ -120,6 +123,7 @@ export declare function useConversationList(options: ConversationListOptions): {
|
|
|
120
123
|
active: boolean;
|
|
121
124
|
draggable: boolean;
|
|
122
125
|
userImage: string | undefined;
|
|
126
|
+
userName: string | undefined;
|
|
123
127
|
selectConversation: ((id: string) => void | Promise<void>) | undefined;
|
|
124
128
|
copyLink: (conversationId: string) => Promise<void>;
|
|
125
129
|
debug: (() => void) | undefined;
|
|
@@ -492,11 +492,13 @@ export function useConversationList(options) {
|
|
|
492
492
|
return draggedConversationId.value !== null && dropTargetGroupId.value === groupId;
|
|
493
493
|
}
|
|
494
494
|
function getConversationItemProps(conversation) {
|
|
495
|
+
const author = options.getMember?.(conversation.userId);
|
|
495
496
|
return {
|
|
496
497
|
conversation,
|
|
497
498
|
active: conversation.id === options.currentId,
|
|
498
499
|
draggable: true,
|
|
499
|
-
userImage:
|
|
500
|
+
userImage: author?.image,
|
|
501
|
+
userName: author?.name,
|
|
500
502
|
selectConversation: options.selectConversation,
|
|
501
503
|
copyLink: copyConversationLink,
|
|
502
504
|
debug: options.showDebugOption ? () => {
|
|
@@ -85,7 +85,7 @@ const route = useRoute();
|
|
|
85
85
|
const embedConfig = useEmbedConfig();
|
|
86
86
|
const chatAction = useChatAction();
|
|
87
87
|
const { user: authUser, activeOrganization, logout } = useChatAuth();
|
|
88
|
-
const { getMember
|
|
88
|
+
const { getMember } = useWorkspaceMembers();
|
|
89
89
|
const statusToast = useStatusToast();
|
|
90
90
|
const {
|
|
91
91
|
conversations,
|
|
@@ -928,7 +928,7 @@ async function handleDelete() {
|
|
|
928
928
|
<ChatConversationList
|
|
929
929
|
:conversations="visibleConversations"
|
|
930
930
|
:current-id="activeConversationId ?? void 0"
|
|
931
|
-
:get-member
|
|
931
|
+
:get-member="getMember"
|
|
932
932
|
:select-conversation="openConversation"
|
|
933
933
|
:reset-conversation="handleNewConversation"
|
|
934
934
|
:start-conversation="handleNewConversation"
|
|
@@ -42,6 +42,7 @@ const props = defineProps({
|
|
|
42
42
|
user: { type: null, required: false },
|
|
43
43
|
features: { type: Object, required: false },
|
|
44
44
|
variant: { type: String, required: false },
|
|
45
|
+
position: { type: String, required: false },
|
|
45
46
|
defaultOpen: { type: Boolean, required: false },
|
|
46
47
|
closeOnOutsideClick: { type: Boolean, required: false },
|
|
47
48
|
keepMounted: { type: Boolean, required: false },
|
|
@@ -34,6 +34,7 @@ const { hideSidebar = void 0, syncBrowserState = true, closeOnOutsideClick = tru
|
|
|
34
34
|
user: { type: null, required: false },
|
|
35
35
|
features: { type: Object, required: false },
|
|
36
36
|
variant: { type: String, required: false },
|
|
37
|
+
position: { type: String, required: false },
|
|
37
38
|
defaultOpen: { type: Boolean, required: false },
|
|
38
39
|
closeOnOutsideClick: { type: Boolean, required: false },
|
|
39
40
|
keepMounted: { type: Boolean, required: false },
|
|
@@ -132,7 +133,7 @@ const widgetEmbedProps = computed(() => ({
|
|
|
132
133
|
|
|
133
134
|
<ClientOnly v-else>
|
|
134
135
|
<!-- Floating: popover anchored to the launcher pill -->
|
|
135
|
-
<TelaChatFloatingWindow v-if="variant === 'floating'" :open="isOpen" :close-on-outside-click="closeOnOutsideClick" :keep-mounted="props.keepMounted" @update:open="setOpen">
|
|
136
|
+
<TelaChatFloatingWindow v-if="variant === 'floating'" :open="isOpen" :position="props.position" :close-on-outside-click="closeOnOutsideClick" :keep-mounted="props.keepMounted" @update:open="setOpen">
|
|
136
137
|
<template #launcher>
|
|
137
138
|
<TelaChatFloatingWindowTrigger :open="isOpen" :label="props.launcherLabel" />
|
|
138
139
|
</template>
|
|
@@ -67,8 +67,8 @@ declare const __VLS_base: import("vue").DefineComponent<__VLS_PublicProps, {}, {
|
|
|
67
67
|
copyLink: () => any;
|
|
68
68
|
debug: () => any;
|
|
69
69
|
selectConversation: (id: string) => any;
|
|
70
|
-
send: (content: string, files: FileUploadInput[], agentInputs?: TelaAgentExecutionInput[] | undefined) => any;
|
|
71
70
|
settings: () => any;
|
|
71
|
+
send: (content: string, files: FileUploadInput[], agentInputs?: TelaAgentExecutionInput[] | undefined) => any;
|
|
72
72
|
tabChange: (tab: TopbarTab) => any;
|
|
73
73
|
newConversation: (groupId?: string | null | undefined) => any;
|
|
74
74
|
switchWorkspace: () => any;
|
|
@@ -88,8 +88,8 @@ declare const __VLS_base: import("vue").DefineComponent<__VLS_PublicProps, {}, {
|
|
|
88
88
|
onCopyLink?: (() => any) | undefined;
|
|
89
89
|
onDebug?: (() => any) | undefined;
|
|
90
90
|
onSelectConversation?: ((id: string) => any) | undefined;
|
|
91
|
-
onSend?: ((content: string, files: FileUploadInput[], agentInputs?: TelaAgentExecutionInput[] | undefined) => any) | undefined;
|
|
92
91
|
onSettings?: (() => any) | undefined;
|
|
92
|
+
onSend?: ((content: string, files: FileUploadInput[], agentInputs?: TelaAgentExecutionInput[] | undefined) => any) | undefined;
|
|
93
93
|
"onUpdate:selectedModel"?: ((value: string | null) => any) | undefined;
|
|
94
94
|
"onUpdate:selectedReasoningEffort"?: ((value: any) => any) | undefined;
|
|
95
95
|
onTabChange?: ((tab: TopbarTab) => any) | undefined;
|
|
@@ -67,8 +67,8 @@ declare const __VLS_base: import("vue").DefineComponent<__VLS_PublicProps, {}, {
|
|
|
67
67
|
copyLink: () => any;
|
|
68
68
|
debug: () => any;
|
|
69
69
|
selectConversation: (id: string) => any;
|
|
70
|
-
send: (content: string, files: FileUploadInput[], agentInputs?: TelaAgentExecutionInput[] | undefined) => any;
|
|
71
70
|
settings: () => any;
|
|
71
|
+
send: (content: string, files: FileUploadInput[], agentInputs?: TelaAgentExecutionInput[] | undefined) => any;
|
|
72
72
|
tabChange: (tab: TopbarTab) => any;
|
|
73
73
|
newConversation: (groupId?: string | null | undefined) => any;
|
|
74
74
|
switchWorkspace: () => any;
|
|
@@ -88,8 +88,8 @@ declare const __VLS_base: import("vue").DefineComponent<__VLS_PublicProps, {}, {
|
|
|
88
88
|
onCopyLink?: (() => any) | undefined;
|
|
89
89
|
onDebug?: (() => any) | undefined;
|
|
90
90
|
onSelectConversation?: ((id: string) => any) | undefined;
|
|
91
|
-
onSend?: ((content: string, files: FileUploadInput[], agentInputs?: TelaAgentExecutionInput[] | undefined) => any) | undefined;
|
|
92
91
|
onSettings?: (() => any) | undefined;
|
|
92
|
+
onSend?: ((content: string, files: FileUploadInput[], agentInputs?: TelaAgentExecutionInput[] | undefined) => any) | undefined;
|
|
93
93
|
"onUpdate:selectedModel"?: ((value: string | null) => any) | undefined;
|
|
94
94
|
"onUpdate:selectedReasoningEffort"?: ((value: any) => any) | undefined;
|
|
95
95
|
onTabChange?: ((tab: TopbarTab) => any) | undefined;
|
|
@@ -189,6 +189,8 @@ export type ChatEmbedVariantProps = {
|
|
|
189
189
|
* client-only and default to `hideSidebar`.
|
|
190
190
|
*/
|
|
191
191
|
variant?: ChatEmbedVariant;
|
|
192
|
+
/** Floating variant only: launcher corner and animation origin. Defaults to right. */
|
|
193
|
+
position?: 'left' | 'right';
|
|
192
194
|
/** Floating variant only: start with the widget window open. */
|
|
193
195
|
defaultOpen?: boolean;
|
|
194
196
|
/** Floating variant only: close on outside clicks. Defaults to true. */
|
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
import { applyLightweightPart } from '../../../types/schemas/integrations/lightweight.js';
|
|
2
2
|
import type { LightweightTurn } from '../../../types/schemas/integrations/lightweight.js';
|
|
3
|
+
import { createMessagePartsAccumulator as createClaudeMessagePartsAccumulator } from '../../../messages/composables/use-direct-stream.js';
|
|
3
4
|
import type { DirectStreamOptions, MessagePartsEvent } from '../../../messages/composables/use-direct-stream.js';
|
|
4
5
|
type MessagePartsAccumulator = {
|
|
5
6
|
runId: string | null;
|
|
6
7
|
lightweight?: LightweightTurn;
|
|
8
|
+
claude?: ReturnType<typeof createClaudeMessagePartsAccumulator>;
|
|
7
9
|
};
|
|
8
10
|
export declare function createMessagePartsAccumulator(runId?: string | null): MessagePartsAccumulator;
|
|
9
11
|
export declare function applyMessagePartsEvent(accumulator: MessagePartsAccumulator, input: MessagePartsEvent): {
|
|
10
|
-
text:
|
|
11
|
-
thinking:
|
|
12
|
+
text: string;
|
|
13
|
+
thinking: string;
|
|
12
14
|
changed: boolean;
|
|
13
|
-
reasoning?: undefined;
|
|
14
|
-
files?: undefined;
|
|
15
15
|
} | {
|
|
16
16
|
text: any;
|
|
17
17
|
thinking: any;
|
|
@@ -1,13 +1,20 @@
|
|
|
1
1
|
import { applyLightweightPart, createLightweightTurn } from "../../../types/schemas/integrations/lightweight.js";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
applyMessagePartsEvent as applyClaudeMessagePartsEvent,
|
|
4
|
+
createMessagePartsAccumulator as createClaudeMessagePartsAccumulator,
|
|
5
|
+
useDirectStreamWithProtocol
|
|
6
|
+
} from "../../../messages/composables/use-direct-stream.js";
|
|
3
7
|
export function createMessagePartsAccumulator(runId = null) {
|
|
4
8
|
return {
|
|
5
9
|
runId
|
|
6
10
|
};
|
|
7
11
|
}
|
|
8
12
|
export function applyMessagePartsEvent(accumulator, input) {
|
|
9
|
-
if (input.harness !== "lightweight" && accumulator.lightweight?.runId !== input.runId)
|
|
10
|
-
|
|
13
|
+
if (input.harness !== "lightweight" && accumulator.lightweight?.runId !== input.runId) {
|
|
14
|
+
accumulator.claude ??= createClaudeMessagePartsAccumulator();
|
|
15
|
+
accumulator.runId = input.runId;
|
|
16
|
+
return applyClaudeMessagePartsEvent(accumulator.claude, input);
|
|
17
|
+
}
|
|
11
18
|
if (accumulator.lightweight?.runId !== input.runId)
|
|
12
19
|
accumulator.lightweight = createLightweightTurn(input.runId, input.sessionId ?? "");
|
|
13
20
|
accumulator.runId = input.runId;
|
|
@@ -1,24 +1,37 @@
|
|
|
1
1
|
<script setup>
|
|
2
|
+
import { formatMessageMenuTimestamp } from "../message-timestamps";
|
|
2
3
|
defineProps({
|
|
4
|
+
createdAt: { type: [Date, String], required: true },
|
|
5
|
+
forkEnabled: { type: Boolean, required: false },
|
|
3
6
|
disabled: { type: Boolean, required: false }
|
|
4
7
|
});
|
|
5
8
|
const emit = defineEmits(["fork"]);
|
|
6
9
|
</script>
|
|
7
10
|
|
|
8
11
|
<template>
|
|
9
|
-
<div>
|
|
10
|
-
<
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
12
|
+
<div data-allow-mismatch @click.stop>
|
|
13
|
+
<TelaDropdownMenuRoot :modal="false">
|
|
14
|
+
<TelaDropdownMenuTrigger as-child class="data-[state=open]:bg-muted">
|
|
15
|
+
<TelaIconButton
|
|
16
|
+
icon="i-ph-dots-three"
|
|
17
|
+
color="secondary"
|
|
18
|
+
size="xs"
|
|
19
|
+
aria-label="Ações da mensagem"
|
|
20
|
+
/>
|
|
21
|
+
</TelaDropdownMenuTrigger>
|
|
22
|
+
<TelaDropdownMenuContent align="end" class="!min-w-128px !z-605">
|
|
23
|
+
<TelaDropdownMenuLabel body-12-regular text-tertiary class="!font-normal !pb-4px">
|
|
24
|
+
{{ formatMessageMenuTimestamp(createdAt) }}
|
|
25
|
+
</TelaDropdownMenuLabel>
|
|
26
|
+
<TelaDropdownMenuItem
|
|
27
|
+
v-if="forkEnabled"
|
|
28
|
+
icon="i-ph-git-branch"
|
|
29
|
+
:disabled="disabled"
|
|
30
|
+
@click="emit('fork')"
|
|
31
|
+
>
|
|
32
|
+
Continuar em nova conversa
|
|
33
|
+
</TelaDropdownMenuItem>
|
|
34
|
+
</TelaDropdownMenuContent>
|
|
35
|
+
</TelaDropdownMenuRoot>
|
|
23
36
|
</div>
|
|
24
37
|
</template>
|
|
@@ -328,7 +328,9 @@ const attachedFiles = computed(() => {
|
|
|
328
328
|
</span>
|
|
329
329
|
<ChatMessageFeedback v-if="hasMessageFeedback" :message="message" />
|
|
330
330
|
<ChatMessageActions
|
|
331
|
-
v-if="
|
|
331
|
+
v-if="message.role === 'assistant' && message.status === 'completed' && !streamActive"
|
|
332
|
+
:created-at="message.createdAt"
|
|
333
|
+
:fork-enabled="forkEnabled"
|
|
332
334
|
:disabled="forkDisabled"
|
|
333
335
|
@fork="emit('fork', message.id)"
|
|
334
336
|
/>
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
import ChatMessageBubble from "./chat-message-bubble.vue";
|
|
3
3
|
import { useStickToBottom } from "../composables/use-stick-to-bottom";
|
|
4
4
|
import { mergeConversationTimeline } from "../conversation-timeline";
|
|
5
|
+
import { getTimestampSeparatorLabels } from "../message-timestamps";
|
|
6
|
+
import { useCurrentDay } from "../composables/current-day";
|
|
5
7
|
const props = defineProps({
|
|
6
8
|
messages: { type: Array, required: true },
|
|
7
9
|
events: { type: [Array, null], required: false },
|
|
@@ -45,6 +47,8 @@ const skeletonMessages = [
|
|
|
45
47
|
{ id: "assistant-2", align: "start", width: "84%", height: "92px" }
|
|
46
48
|
];
|
|
47
49
|
const timelineItems = computed(() => mergeConversationTimeline(props.messages, props.events));
|
|
50
|
+
const today = useCurrentDay();
|
|
51
|
+
const timestampSeparators = computed(() => getTimestampSeparatorLabels(props.messages, today.value));
|
|
48
52
|
const latestMessageId = computed(() => props.messages.at(-1)?.id ?? null);
|
|
49
53
|
const latestOutputSignal = computed(() => {
|
|
50
54
|
const latestMessage = props.messages.at(-1);
|
|
@@ -112,6 +116,16 @@ defineExpose({
|
|
|
112
116
|
|
|
113
117
|
<template v-else>
|
|
114
118
|
<template v-for="item in timelineItems" :key="`${item.kind}:${item.id}`">
|
|
119
|
+
<p
|
|
120
|
+
v-if="item.kind === 'message' && timestampSeparators.has(item.id)"
|
|
121
|
+
data-chat-timestamp-separator
|
|
122
|
+
my-8px
|
|
123
|
+
text-center
|
|
124
|
+
body-12-regular
|
|
125
|
+
text-tertiary
|
|
126
|
+
>
|
|
127
|
+
{{ timestampSeparators.get(item.id) }}
|
|
128
|
+
</p>
|
|
115
129
|
<ChatConversationEvent
|
|
116
130
|
v-if="item.kind === 'event'"
|
|
117
131
|
:event="item.value"
|
|
@@ -27,24 +27,25 @@ const TOOL_WIDGETS = {
|
|
|
27
27
|
"grep": ChatGrepWidget,
|
|
28
28
|
"generic": ChatGenericToolWidget
|
|
29
29
|
};
|
|
30
|
-
const
|
|
30
|
+
const toolGroup = computed(() => typeof props.step !== "string" && props.step.type === "tool_group" ? props.step : null);
|
|
31
|
+
const displayToolUse = computed(() => toolGroup.value ? withReportedToolInput(toolGroup.value.toolUse, toolGroup.value.toolResult) : null);
|
|
31
32
|
const toolDisplay = computed(() => {
|
|
32
|
-
if (
|
|
33
|
+
if (!toolGroup.value)
|
|
33
34
|
return null;
|
|
34
|
-
return resolveToolDisplay(
|
|
35
|
+
return resolveToolDisplay(toolGroup.value.toolUse);
|
|
35
36
|
});
|
|
36
37
|
const stepWasExecuted = computed(
|
|
37
|
-
() =>
|
|
38
|
+
() => toolGroup.value?.toolResult !== void 0 && !props.isActive
|
|
38
39
|
);
|
|
39
40
|
const stepMeta = computed(() => {
|
|
40
|
-
if (
|
|
41
|
+
if (!toolGroup.value)
|
|
41
42
|
return null;
|
|
42
43
|
return resolveToolMeta(displayToolUse.value);
|
|
43
44
|
});
|
|
44
45
|
const stepTitle = computed(() => {
|
|
45
|
-
if (
|
|
46
|
+
if (!toolGroup.value)
|
|
46
47
|
return "";
|
|
47
|
-
return resolveToolTitle(
|
|
48
|
+
return resolveToolTitle(toolGroup.value.toolUse, stepWasExecuted.value);
|
|
48
49
|
});
|
|
49
50
|
</script>
|
|
50
51
|
|
|
@@ -61,6 +62,21 @@ const stepTitle = computed(() => {
|
|
|
61
62
|
/>
|
|
62
63
|
</div>
|
|
63
64
|
|
|
65
|
+
<div v-else-if="step.type === 'thinking'" class="chat-reasoning-step">
|
|
66
|
+
<ChatReasoningToolRow
|
|
67
|
+
:title="isActive && !completed ? 'Pensando' : 'Pensou'"
|
|
68
|
+
has-result
|
|
69
|
+
:is-active="isActive && !completed"
|
|
70
|
+
>
|
|
71
|
+
<ChatThinkingWidget
|
|
72
|
+
class="chat-reasoning-thinking-detail"
|
|
73
|
+
:content="step.text"
|
|
74
|
+
:is-active="isActive"
|
|
75
|
+
:completed="completed"
|
|
76
|
+
/>
|
|
77
|
+
</ChatReasoningToolRow>
|
|
78
|
+
</div>
|
|
79
|
+
|
|
64
80
|
<div v-else class="chat-reasoning-step">
|
|
65
81
|
<ChatReasoningToolRow
|
|
66
82
|
:title="stepTitle"
|
|
@@ -34,6 +34,9 @@ const currentStepName = computed(() => {
|
|
|
34
34
|
if (step && typeof step !== "string" && step.type === "tool_group") {
|
|
35
35
|
return resolveToolTitle(step.toolUse, false);
|
|
36
36
|
}
|
|
37
|
+
if (step && typeof step !== "string" && step.type === "thinking") {
|
|
38
|
+
return "Pensando";
|
|
39
|
+
}
|
|
37
40
|
if (step && typeof step === "string") {
|
|
38
41
|
const truncated = step.length > TEXT_TRUNCATE_LENGTH ? `${step.slice(0, TEXT_TRUNCATE_LENGTH)}...` : step;
|
|
39
42
|
return truncated;
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
} from "../message-reasoning.js";
|
|
9
9
|
import {
|
|
10
10
|
beginAssistantMessageRetry,
|
|
11
|
+
createOptimisticMessagePair,
|
|
11
12
|
markPendingAssistantMessagesFailed,
|
|
12
13
|
markPendingAssistantMessagesInterrupted,
|
|
13
14
|
restoreAssistantMessageAfterRetryFailure,
|
|
@@ -341,7 +342,7 @@ export function useChat(options = {}, adapter = {}) {
|
|
|
341
342
|
if (generation !== pollGeneration)
|
|
342
343
|
return;
|
|
343
344
|
const statusCode = getChatApiErrorStatus(err);
|
|
344
|
-
const terminalError = statusCode === 501 || statusCode !== null && statusCode >= 400 && statusCode < 500 && statusCode !== 408 && statusCode !== 429;
|
|
345
|
+
const terminalError = statusCode === 501 || statusCode !== null && statusCode >= 400 && statusCode < 500 && statusCode !== 401 && statusCode !== 408 && statusCode !== 429;
|
|
345
346
|
consecutivePollFailures++;
|
|
346
347
|
pollOutageStartedAt ??= Date.now();
|
|
347
348
|
pollInterval.value = Math.min(POLL_INTERVAL_MS * 2 ** consecutivePollFailures, POLL_MAX_BACKOFF_MS);
|
|
@@ -531,36 +532,14 @@ export function useChat(options = {}, adapter = {}) {
|
|
|
531
532
|
const conversationId = currentConversation.value?.id ?? "";
|
|
532
533
|
const existingOptimisticUserMessage = queuedSend ? messages.value.find((message) => message.id === queuedSend.optimisticIds.user) : void 0;
|
|
533
534
|
const existingOptimisticAssistantMessage = queuedSend ? messages.value.find((message) => message.id === queuedSend.optimisticIds.assistant) : void 0;
|
|
534
|
-
const
|
|
535
|
-
id: crypto.randomUUID(),
|
|
535
|
+
const optimisticPair = createOptimisticMessagePair({
|
|
536
536
|
conversationId,
|
|
537
|
-
role: "user",
|
|
538
537
|
content,
|
|
539
|
-
status: "completed",
|
|
540
|
-
reasoningData: null,
|
|
541
538
|
files: filesWithSource,
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
generationStartedAt: null,
|
|
547
|
-
updatedAt: null
|
|
548
|
-
};
|
|
549
|
-
const optimisticAssistantMessage = existingOptimisticAssistantMessage ?? {
|
|
550
|
-
id: crypto.randomUUID(),
|
|
551
|
-
conversationId,
|
|
552
|
-
role: "assistant",
|
|
553
|
-
content: "",
|
|
554
|
-
status: "pending",
|
|
555
|
-
reasoningData: null,
|
|
556
|
-
files: null,
|
|
557
|
-
externalUuid: null,
|
|
558
|
-
ttft: null,
|
|
559
|
-
createdBy: null,
|
|
560
|
-
createdAt: /* @__PURE__ */ new Date(),
|
|
561
|
-
generationStartedAt: /* @__PURE__ */ new Date(),
|
|
562
|
-
updatedAt: null
|
|
563
|
-
};
|
|
539
|
+
createdBy: authUser.value?.email ?? null
|
|
540
|
+
});
|
|
541
|
+
const optimisticUserMessage = existingOptimisticUserMessage ?? optimisticPair.user;
|
|
542
|
+
const optimisticAssistantMessage = existingOptimisticAssistantMessage ?? optimisticPair.assistant;
|
|
564
543
|
streamingMessageId.value = optimisticAssistantMessage.id;
|
|
565
544
|
directStream.beginMessage();
|
|
566
545
|
if (!queuedSend)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function useCurrentDay(): import("vue").Ref<Date, Date>;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { onScopeDispose, ref } from "vue";
|
|
2
|
+
function msUntilNextMidnight(now) {
|
|
3
|
+
return new Date(now.getFullYear(), now.getMonth(), now.getDate() + 1).getTime() - now.getTime();
|
|
4
|
+
}
|
|
5
|
+
export function useCurrentDay() {
|
|
6
|
+
const today = ref(/* @__PURE__ */ new Date());
|
|
7
|
+
let timeout;
|
|
8
|
+
function scheduleNextDay() {
|
|
9
|
+
timeout = setTimeout(() => {
|
|
10
|
+
today.value = /* @__PURE__ */ new Date();
|
|
11
|
+
scheduleNextDay();
|
|
12
|
+
}, msUntilNextMidnight(today.value));
|
|
13
|
+
}
|
|
14
|
+
scheduleNextDay();
|
|
15
|
+
onScopeDispose(() => clearTimeout(timeout));
|
|
16
|
+
return today;
|
|
17
|
+
}
|
|
@@ -8,6 +8,7 @@ import { applyLightweightPart, createLightweightTurn } from "../../types/schemas
|
|
|
8
8
|
import { applyMessagePartsEvent as applyLightweightEvent, createMessagePartsAccumulator as createLightweightAccumulator } from "../../lightweight/messages/composables/use-direct-stream.js";
|
|
9
9
|
import { resolveReasoningDisplayMessages } from "../message-reasoning.js";
|
|
10
10
|
import { overlayStreamedMessage } from "../stream-overlay.js";
|
|
11
|
+
import { createOptimisticMessagePair } from "../message-state.js";
|
|
11
12
|
export const EMPTY_CHAT_DEBUG_PLAYBACK_STATE = {
|
|
12
13
|
messages: [],
|
|
13
14
|
loading: false,
|
|
@@ -589,6 +590,17 @@ export function createChatDebugScenarios(baseTime = Date.now()) {
|
|
|
589
590
|
role: "assistant",
|
|
590
591
|
status: "pending"
|
|
591
592
|
});
|
|
593
|
+
const optimisticSendIds = ["ffffffff-0000-4000-8000-000000000001", "00000000-0000-4000-8000-000000000002"];
|
|
594
|
+
const optimisticSend = createOptimisticMessagePair({
|
|
595
|
+
conversationId: "debug-conversation",
|
|
596
|
+
content: "Resuma o parecer jur\xEDdico anexado em tr\xEAs pontos.",
|
|
597
|
+
files: null,
|
|
598
|
+
createdBy: "debugger@tela.com",
|
|
599
|
+
now: new Date(baseTime),
|
|
600
|
+
createId: () => optimisticSendIds.shift()
|
|
601
|
+
});
|
|
602
|
+
const acknowledgedSendAt = new Date(baseTime + 40);
|
|
603
|
+
const acknowledgedAssistantAt = new Date(acknowledgedSendAt.getTime() + 1);
|
|
592
604
|
const allToolsReasoning = allToolTypeReasoning();
|
|
593
605
|
const allToolsIntro = allToolsReasoning[0];
|
|
594
606
|
const allToolGroups = allToolsReasoning.slice(1);
|
|
@@ -762,6 +774,99 @@ export function createChatDebugScenarios(baseTime = Date.now()) {
|
|
|
762
774
|
})
|
|
763
775
|
]
|
|
764
776
|
},
|
|
777
|
+
{
|
|
778
|
+
id: "optimistic-send-order",
|
|
779
|
+
name: "Optimistic send order",
|
|
780
|
+
intent: 'A fresh send renders prompt then "Trabalhando" from the first frame, and keeps that order when the server swaps in persisted ids.',
|
|
781
|
+
durationMs: 5500,
|
|
782
|
+
events: [
|
|
783
|
+
event({
|
|
784
|
+
id: "optimistic-send-user",
|
|
785
|
+
atMs: 0,
|
|
786
|
+
label: "Optimistic user message",
|
|
787
|
+
tone: "user",
|
|
788
|
+
type: "message.upsert",
|
|
789
|
+
message: optimisticSend.user
|
|
790
|
+
}),
|
|
791
|
+
event({
|
|
792
|
+
id: "optimistic-send-assistant",
|
|
793
|
+
atMs: 0,
|
|
794
|
+
label: "Optimistic assistant",
|
|
795
|
+
tone: "assistant",
|
|
796
|
+
type: "message.upsert",
|
|
797
|
+
message: optimisticSend.assistant
|
|
798
|
+
}),
|
|
799
|
+
event({
|
|
800
|
+
id: "optimistic-send-stream",
|
|
801
|
+
atMs: 0,
|
|
802
|
+
label: "Stream target",
|
|
803
|
+
tone: "neutral",
|
|
804
|
+
type: "playback.patch",
|
|
805
|
+
patch: { streamingActiveMessageId: optimisticSend.assistant.id }
|
|
806
|
+
}),
|
|
807
|
+
event({
|
|
808
|
+
id: "optimistic-send-ack-user",
|
|
809
|
+
atMs: 2500,
|
|
810
|
+
label: "Server persists user",
|
|
811
|
+
tone: "user",
|
|
812
|
+
type: "message.patch",
|
|
813
|
+
messageId: optimisticSend.user.id,
|
|
814
|
+
patch: { id: "optimistic-send-user", createdAt: acknowledgedSendAt }
|
|
815
|
+
}),
|
|
816
|
+
event({
|
|
817
|
+
id: "optimistic-send-ack-assistant",
|
|
818
|
+
atMs: 2500,
|
|
819
|
+
label: "Server persists assistant",
|
|
820
|
+
tone: "assistant",
|
|
821
|
+
type: "message.patch",
|
|
822
|
+
messageId: optimisticSend.assistant.id,
|
|
823
|
+
patch: { id: "optimistic-send-assistant", createdAt: acknowledgedAssistantAt, generationStartedAt: acknowledgedAssistantAt }
|
|
824
|
+
}),
|
|
825
|
+
event({
|
|
826
|
+
id: "optimistic-send-ack-stream",
|
|
827
|
+
atMs: 2500,
|
|
828
|
+
label: "Stream target persisted",
|
|
829
|
+
tone: "neutral",
|
|
830
|
+
type: "playback.patch",
|
|
831
|
+
patch: { streamingActiveMessageId: "optimistic-send-assistant" }
|
|
832
|
+
}),
|
|
833
|
+
event({
|
|
834
|
+
id: "optimistic-send-reasoning",
|
|
835
|
+
atMs: 3300,
|
|
836
|
+
label: "Reasoning starts",
|
|
837
|
+
tone: "reasoning",
|
|
838
|
+
type: "message.patch",
|
|
839
|
+
messageId: "optimistic-send-assistant",
|
|
840
|
+
patch: { reasoningData: ["Lendo o parecer anexado"] }
|
|
841
|
+
}),
|
|
842
|
+
event({
|
|
843
|
+
id: "optimistic-send-answer",
|
|
844
|
+
atMs: 4300,
|
|
845
|
+
label: "Answer",
|
|
846
|
+
tone: "assistant",
|
|
847
|
+
type: "message.patch",
|
|
848
|
+
messageId: "optimistic-send-assistant",
|
|
849
|
+
patch: { content: "1. O contrato \xE9 v\xE1lido.\n2. A cl\xE1usula penal \xE9 abusiva.\n3. Cabe renegocia\xE7\xE3o." }
|
|
850
|
+
}),
|
|
851
|
+
event({
|
|
852
|
+
id: "optimistic-send-complete",
|
|
853
|
+
atMs: 5e3,
|
|
854
|
+
label: "Message completed",
|
|
855
|
+
tone: "assistant",
|
|
856
|
+
type: "message.patch",
|
|
857
|
+
messageId: "optimistic-send-assistant",
|
|
858
|
+
patch: { status: "completed", updatedAt: new Date(baseTime + 5e3) }
|
|
859
|
+
}),
|
|
860
|
+
event({
|
|
861
|
+
id: "optimistic-send-close",
|
|
862
|
+
atMs: 5e3,
|
|
863
|
+
label: "Stream closes",
|
|
864
|
+
tone: "neutral",
|
|
865
|
+
type: "playback.patch",
|
|
866
|
+
patch: { streamingActiveMessageId: null }
|
|
867
|
+
})
|
|
868
|
+
]
|
|
869
|
+
},
|
|
765
870
|
{
|
|
766
871
|
id: "lightweight-stream",
|
|
767
872
|
name: "Lightweight stream",
|
|
@@ -2,7 +2,6 @@ import type { Message } from '../types/schemas/chat/conversations.js';
|
|
|
2
2
|
import type { ReasoningStep } from './reasoning.js';
|
|
3
3
|
type ParsedReasoning = {
|
|
4
4
|
steps: ReasoningStep[];
|
|
5
|
-
thinkingTexts: Set<string>;
|
|
6
5
|
currentAnswerParts: string[];
|
|
7
6
|
currentAnswerStepTexts: Set<string>;
|
|
8
7
|
};
|
|
@@ -7,6 +7,8 @@ export function createReasoningCache() {
|
|
|
7
7
|
function getReasoningStepKey(step) {
|
|
8
8
|
if (typeof step === "string")
|
|
9
9
|
return `text:${step.slice(0, 100)}`;
|
|
10
|
+
if (step.type === "thinking")
|
|
11
|
+
return `thinking:${step.text.slice(0, 100)}`;
|
|
10
12
|
return `tool:${step.toolUse.id}`;
|
|
11
13
|
}
|
|
12
14
|
function groupReasoningSteps(parsedSteps) {
|
|
@@ -15,6 +17,14 @@ function groupReasoningSteps(parsedSteps) {
|
|
|
15
17
|
for (const step of parsedSteps) {
|
|
16
18
|
if (typeof step === "string") {
|
|
17
19
|
grouped.push(step);
|
|
20
|
+
} else if ("type" in step) {
|
|
21
|
+
const previous = grouped.at(-1);
|
|
22
|
+
if (typeof previous === "object" && previous.type === "thinking")
|
|
23
|
+
grouped[grouped.length - 1] = { type: "thinking", text: `${previous.text}
|
|
24
|
+
|
|
25
|
+
${step.text}` };
|
|
26
|
+
else
|
|
27
|
+
grouped.push(step);
|
|
18
28
|
} else if ("name" in step && "input" in step) {
|
|
19
29
|
pendingToolUses.set(step.id, step);
|
|
20
30
|
grouped.push({
|
|
@@ -77,7 +87,7 @@ function parseReasoningFromPolling(reasoning) {
|
|
|
77
87
|
const allSteps = [];
|
|
78
88
|
const seenContentIds = /* @__PURE__ */ new Set();
|
|
79
89
|
const seenTexts = /* @__PURE__ */ new Set();
|
|
80
|
-
const
|
|
90
|
+
const seenThoughts = /* @__PURE__ */ new Set();
|
|
81
91
|
let currentAnswerParts = [];
|
|
82
92
|
let currentAnswerStepTexts = /* @__PURE__ */ new Set();
|
|
83
93
|
for (const item of reasoning) {
|
|
@@ -117,17 +127,18 @@ function parseReasoningFromPolling(reasoning) {
|
|
|
117
127
|
if (!contentPart || typeof contentPart !== "object")
|
|
118
128
|
continue;
|
|
119
129
|
const part = contentPart;
|
|
120
|
-
if (
|
|
121
|
-
if (part.
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
130
|
+
if (part.type === "thinking" && typeof part.text === "string") {
|
|
131
|
+
if (seenThoughts.has(part.text))
|
|
132
|
+
continue;
|
|
133
|
+
seenThoughts.add(part.text);
|
|
134
|
+
allSteps.push({ type: "thinking", text: part.text });
|
|
135
|
+
} else if (part.type === "text" && typeof part.text === "string") {
|
|
136
|
+
answerParts.push(part.text);
|
|
125
137
|
if (seenTexts.has(part.text))
|
|
126
138
|
continue;
|
|
127
139
|
seenTexts.add(part.text);
|
|
128
140
|
allSteps.push(part.text);
|
|
129
|
-
|
|
130
|
-
answerStepTexts.add(part.text);
|
|
141
|
+
answerStepTexts.add(part.text);
|
|
131
142
|
} else if (part.type === "tool_use" && part.id && part.name) {
|
|
132
143
|
const toolId = part.id;
|
|
133
144
|
if (seenContentIds.has(`tool_use:${toolId}`))
|
|
@@ -183,7 +194,7 @@ function parseReasoningFromPolling(reasoning) {
|
|
|
183
194
|
});
|
|
184
195
|
}
|
|
185
196
|
}
|
|
186
|
-
return { steps: groupReasoningSteps(allSteps),
|
|
197
|
+
return { steps: groupReasoningSteps(allSteps), currentAnswerParts, currentAnswerStepTexts };
|
|
187
198
|
}
|
|
188
199
|
export function resolveReasoningDisplayMessages(messages, cache = createReasoningCache()) {
|
|
189
200
|
pruneReasoningCache(cache, messages);
|
|
@@ -198,7 +209,7 @@ export function resolveReasoningDisplayMessages(messages, cache = createReasonin
|
|
|
198
209
|
const key = getReasoningStepKey(step);
|
|
199
210
|
if (renderedSteps.has(key))
|
|
200
211
|
continue;
|
|
201
|
-
if (typeof step === "string" && currentAnswerMatches && parsed.currentAnswerStepTexts.has(step)
|
|
212
|
+
if (typeof step === "string" && currentAnswerMatches && parsed.currentAnswerStepTexts.has(step)) {
|
|
202
213
|
continue;
|
|
203
214
|
}
|
|
204
215
|
renderedSteps.add(key);
|
|
@@ -2,6 +2,17 @@ import type { Message } from '../types/schemas/chat/conversations.js';
|
|
|
2
2
|
export declare const INTERRUPTED_GENERATION_TEXT = "_Gera\u00E7\u00E3o interrompida pelo usu\u00E1rio_";
|
|
3
3
|
export declare const GENERATION_FAILED_TEXT = "Erro: N\u00E3o foi poss\u00EDvel concluir a resposta";
|
|
4
4
|
export declare const CANCELLATION_FAILED_TEXT = "Erro: N\u00E3o foi poss\u00EDvel cancelar a mensagem";
|
|
5
|
+
export declare function createOptimisticMessagePair(input: {
|
|
6
|
+
conversationId: string;
|
|
7
|
+
content: string;
|
|
8
|
+
files: Message['files'];
|
|
9
|
+
createdBy: string | null;
|
|
10
|
+
now?: Date;
|
|
11
|
+
createId?: () => string;
|
|
12
|
+
}): {
|
|
13
|
+
user: Message;
|
|
14
|
+
assistant: Message;
|
|
15
|
+
};
|
|
5
16
|
export declare function markPendingAssistantMessagesInterrupted(messages: Message[]): Message[];
|
|
6
17
|
export declare function markPendingAssistantMessagesFailed(messages: Message[], content?: string): Message[];
|
|
7
18
|
export declare function beginAssistantMessageRetry(messages: Message[], messageId: string): {
|
|
@@ -1,6 +1,43 @@
|
|
|
1
1
|
export const INTERRUPTED_GENERATION_TEXT = "_Gera\xE7\xE3o interrompida pelo usu\xE1rio_";
|
|
2
2
|
export const GENERATION_FAILED_TEXT = "Erro: N\xE3o foi poss\xEDvel concluir a resposta";
|
|
3
3
|
export const CANCELLATION_FAILED_TEXT = "Erro: N\xE3o foi poss\xEDvel cancelar a mensagem";
|
|
4
|
+
export function createOptimisticMessagePair(input) {
|
|
5
|
+
const createId = input.createId ?? (() => crypto.randomUUID());
|
|
6
|
+
const userCreatedAt = input.now ?? /* @__PURE__ */ new Date();
|
|
7
|
+
const assistantCreatedAt = new Date(userCreatedAt.getTime() + 1);
|
|
8
|
+
return {
|
|
9
|
+
user: {
|
|
10
|
+
id: createId(),
|
|
11
|
+
conversationId: input.conversationId,
|
|
12
|
+
role: "user",
|
|
13
|
+
content: input.content,
|
|
14
|
+
status: "completed",
|
|
15
|
+
reasoningData: null,
|
|
16
|
+
files: input.files,
|
|
17
|
+
externalUuid: null,
|
|
18
|
+
ttft: null,
|
|
19
|
+
createdBy: input.createdBy,
|
|
20
|
+
createdAt: userCreatedAt,
|
|
21
|
+
generationStartedAt: null,
|
|
22
|
+
updatedAt: null
|
|
23
|
+
},
|
|
24
|
+
assistant: {
|
|
25
|
+
id: createId(),
|
|
26
|
+
conversationId: input.conversationId,
|
|
27
|
+
role: "assistant",
|
|
28
|
+
content: "",
|
|
29
|
+
status: "pending",
|
|
30
|
+
reasoningData: null,
|
|
31
|
+
files: null,
|
|
32
|
+
externalUuid: null,
|
|
33
|
+
ttft: null,
|
|
34
|
+
createdBy: null,
|
|
35
|
+
createdAt: assistantCreatedAt,
|
|
36
|
+
generationStartedAt: assistantCreatedAt,
|
|
37
|
+
updatedAt: null
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
}
|
|
4
41
|
export function markPendingAssistantMessagesInterrupted(messages) {
|
|
5
42
|
return messages.map((message) => {
|
|
6
43
|
if (message.role !== "assistant" || message.status !== "pending")
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { Message } from '../types/schemas/chat/conversations.js';
|
|
2
|
+
export declare function formatMessageSeparatorTimestamp(value: Date | string, now?: Date): string;
|
|
3
|
+
export declare function formatMessageMenuTimestamp(value: Date | string, now?: Date): string;
|
|
4
|
+
export declare function shouldShowTimestampSeparator(value: Date | string, previousValue: Date | string | null): boolean;
|
|
5
|
+
export declare function getTimestampSeparatorLabels(messages: readonly Pick<Message, 'id' | 'role' | 'createdAt'>[], now?: Date): Map<string, string>;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
const SEPARATOR_GAP_MS = 60 * 60 * 1e3;
|
|
2
|
+
const timeFormat = new Intl.DateTimeFormat("pt-BR", { hour: "2-digit", minute: "2-digit" });
|
|
3
|
+
const weekdayFormat = new Intl.DateTimeFormat("pt-BR", { weekday: "short", day: "numeric", month: "short" });
|
|
4
|
+
const weekdayWithYearFormat = new Intl.DateTimeFormat("pt-BR", { weekday: "short", day: "numeric", month: "short", year: "numeric" });
|
|
5
|
+
const menuFormat = new Intl.DateTimeFormat("pt-BR", { day: "numeric", month: "short", hour: "2-digit", minute: "2-digit" });
|
|
6
|
+
const menuWithYearFormat = new Intl.DateTimeFormat("pt-BR", { day: "numeric", month: "short", year: "numeric", hour: "2-digit", minute: "2-digit" });
|
|
7
|
+
function toDate(value) {
|
|
8
|
+
return value instanceof Date ? value : new Date(value);
|
|
9
|
+
}
|
|
10
|
+
function isSameDay(left, right) {
|
|
11
|
+
return left.getFullYear() === right.getFullYear() && left.getMonth() === right.getMonth() && left.getDate() === right.getDate();
|
|
12
|
+
}
|
|
13
|
+
export function formatMessageSeparatorTimestamp(value, now = /* @__PURE__ */ new Date()) {
|
|
14
|
+
const date = toDate(value);
|
|
15
|
+
const time = timeFormat.format(date);
|
|
16
|
+
if (isSameDay(date, now))
|
|
17
|
+
return `Hoje ${time}`;
|
|
18
|
+
const yesterday = new Date(now.getFullYear(), now.getMonth(), now.getDate() - 1);
|
|
19
|
+
if (isSameDay(date, yesterday))
|
|
20
|
+
return `Ontem ${time}`;
|
|
21
|
+
const day = date.getFullYear() === now.getFullYear() ? weekdayFormat : weekdayWithYearFormat;
|
|
22
|
+
return `${day.format(date)} \xE0s ${time}`;
|
|
23
|
+
}
|
|
24
|
+
export function formatMessageMenuTimestamp(value, now = /* @__PURE__ */ new Date()) {
|
|
25
|
+
const date = toDate(value);
|
|
26
|
+
const format = date.getFullYear() === now.getFullYear() ? menuFormat : menuWithYearFormat;
|
|
27
|
+
return format.format(date);
|
|
28
|
+
}
|
|
29
|
+
export function shouldShowTimestampSeparator(value, previousValue) {
|
|
30
|
+
if (!previousValue)
|
|
31
|
+
return true;
|
|
32
|
+
const date = toDate(value);
|
|
33
|
+
const previous = toDate(previousValue);
|
|
34
|
+
return !isSameDay(date, previous) || date.getTime() - previous.getTime() >= SEPARATOR_GAP_MS;
|
|
35
|
+
}
|
|
36
|
+
export function getTimestampSeparatorLabels(messages, now = /* @__PURE__ */ new Date()) {
|
|
37
|
+
const labels = /* @__PURE__ */ new Map();
|
|
38
|
+
messages.forEach((message, index) => {
|
|
39
|
+
if (message.role === "user" && shouldShowTimestampSeparator(message.createdAt, messages[index - 1]?.createdAt ?? null))
|
|
40
|
+
labels.set(message.id, formatMessageSeparatorTimestamp(message.createdAt, now));
|
|
41
|
+
});
|
|
42
|
+
return labels;
|
|
43
|
+
}
|
|
@@ -14,7 +14,11 @@ export type ToolGroup = {
|
|
|
14
14
|
toolUse: ToolUse;
|
|
15
15
|
toolResult?: ToolResult;
|
|
16
16
|
};
|
|
17
|
-
export type
|
|
17
|
+
export type ThinkingStep = {
|
|
18
|
+
type: 'thinking';
|
|
19
|
+
text: string;
|
|
20
|
+
};
|
|
21
|
+
export type ReasoningStep = string | ToolGroup | ThinkingStep;
|
|
18
22
|
/**
|
|
19
23
|
* A turn that is still working towards its first output has no reasoning of its
|
|
20
24
|
* own yet, so the preparing steps stand in. That makes the wait read as an agent
|
|
@@ -9,6 +9,11 @@ const TOOL_REGISTRY = {
|
|
|
9
9
|
widgetId: "generic",
|
|
10
10
|
labels: { active: "Pesquisando na web", executed: "Pesquisou na web" }
|
|
11
11
|
},
|
|
12
|
+
"super_search_tool": {
|
|
13
|
+
icon: "i-ph-globe",
|
|
14
|
+
widgetId: "generic",
|
|
15
|
+
labels: { active: "Pesquisando em v\xE1rias fontes", executed: "Pesquisou em v\xE1rias fontes" }
|
|
16
|
+
},
|
|
12
17
|
"WebFetch": {
|
|
13
18
|
icon: "i-ph-browser",
|
|
14
19
|
widgetId: "generic",
|
|
@@ -152,6 +157,16 @@ function stringInput(toolUse, ...keys) {
|
|
|
152
157
|
}
|
|
153
158
|
return null;
|
|
154
159
|
}
|
|
160
|
+
function batchedQueries(toolUse) {
|
|
161
|
+
const queries = toolUse.input?.queries;
|
|
162
|
+
if (!Array.isArray(queries))
|
|
163
|
+
return null;
|
|
164
|
+
const filled = queries.filter((query) => typeof query === "string" && query.trim() !== "");
|
|
165
|
+
if (!filled.length)
|
|
166
|
+
return null;
|
|
167
|
+
const first = filled[0].trim();
|
|
168
|
+
return filled.length > 1 ? `${first} +${filled.length - 1}` : first;
|
|
169
|
+
}
|
|
155
170
|
function basename(path) {
|
|
156
171
|
return path.split("/").filter(Boolean).at(-1) ?? path;
|
|
157
172
|
}
|
|
@@ -164,6 +179,8 @@ export function resolveToolMeta(toolUse) {
|
|
|
164
179
|
return stringInput(toolUse, "pattern", "query");
|
|
165
180
|
if (["WebSearch", "WebFetch", "web_search"].includes(toolUse.name))
|
|
166
181
|
return stringInput(toolUse, "query", "url");
|
|
182
|
+
if (toolUse.name === "super_search_tool")
|
|
183
|
+
return stringInput(toolUse, "query") ?? batchedQueries(toolUse);
|
|
167
184
|
if (toolUse.name === "Bash" || toolUse.name === "Execute")
|
|
168
185
|
return stringInput(toolUse, "description", "command");
|
|
169
186
|
if (toolUse.name === "Skill")
|
|
@@ -3,6 +3,7 @@ import MarkdownRender, { removeCustomComponents, setCustomComponents } from "mar
|
|
|
3
3
|
import { wrapCustomMarkdownComponents } from "../markdown/custom-components";
|
|
4
4
|
import { STREAMING_TEXT_FADE_MS } from "../markdown/streaming-text-segments";
|
|
5
5
|
import { escapeSingleDollarSigns } from "../markdown/escape-dollar-signs";
|
|
6
|
+
import { normalizeCustomTagJson } from "../markdown/normalize-custom-tag-json";
|
|
6
7
|
import StreamingTextNode from "../markdown/streaming-text-node.vue";
|
|
7
8
|
const props = defineProps({
|
|
8
9
|
content: { type: [String, null], required: false, default: "" },
|
|
@@ -15,7 +16,7 @@ const props = defineProps({
|
|
|
15
16
|
const attrs = useAttrs();
|
|
16
17
|
const instance = getCurrentInstance();
|
|
17
18
|
const scopeId = `app-markdown-render-${instance?.uid ?? Math.random().toString(36).slice(2)}`;
|
|
18
|
-
const escapedContent = computed(() => escapeSingleDollarSigns(props.content ?? ""));
|
|
19
|
+
const escapedContent = computed(() => escapeSingleDollarSigns(normalizeCustomTagJson(props.content ?? "", props.customHtmlTags)));
|
|
19
20
|
const customMarkdownComponents = computed(() => wrapCustomMarkdownComponents(props.customComponents));
|
|
20
21
|
const streamingTextActive = ref(props.typewriter);
|
|
21
22
|
let streamingTextTimer;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function normalizeCustomTagJson(text: string, customHtmlTags: readonly string[]): string;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export function normalizeCustomTagJson(text, customHtmlTags) {
|
|
2
|
+
if (!customHtmlTags.length)
|
|
3
|
+
return text;
|
|
4
|
+
return text.replace(/(^[ \t]{0,3}(`{3,}|~{3,})[^\n]*\n[\s\S]*?(?:\n[ \t]{0,3}(?=\2)(?:`+|~+)[ \t]*(?=\n|$)|(?![\s\S]))|(`+)[\s\S]*?\3)|^([ \t]{0,3}<([a-z][\w-]*)(?:\s+[\w-]+=(?:"[^"\n]*"|'[^'\n]*'))*\s+[\w-]+=')([\[{][^\n]*[\]}])([ \t]*\/>)[ \t]*(?=\n|$)/gm, (match, code, _fence, _inline, prefix, tag, json, closing) => {
|
|
5
|
+
if (code || !customHtmlTags.includes(tag))
|
|
6
|
+
return match;
|
|
7
|
+
try {
|
|
8
|
+
JSON.parse(json);
|
|
9
|
+
} catch {
|
|
10
|
+
return match;
|
|
11
|
+
}
|
|
12
|
+
const escaped = json.replace(/&/g, "\\u0026").replace(/'/g, "\\u0027").replace(/</g, "\\u003c").replace(/>/g, "\\u003e");
|
|
13
|
+
return `${prefix}${escaped}'${closing}`;
|
|
14
|
+
});
|
|
15
|
+
}
|
|
@@ -206,6 +206,10 @@ export async function proxyChatApiRequest(event, targetPath, options = {}) {
|
|
|
206
206
|
networkErrorCodes: networkErrorCodes(error)
|
|
207
207
|
}, "Chat API proxy upstream request failed");
|
|
208
208
|
}
|
|
209
|
+
if (event.node.res.headersSent) {
|
|
210
|
+
event.node.res.destroy();
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
209
213
|
throw error;
|
|
210
214
|
}
|
|
211
215
|
}
|
|
@@ -99,7 +99,7 @@ export function applyLightweightPart(state, part, sequence, timestamp) {
|
|
|
99
99
|
const id = `${state.runId}:thinking:${typeof part.id === 'string' ? part.id : 'default'}`;
|
|
100
100
|
const existing = state.reasoning.find(item => item.uuid === id)?.message.content[0];
|
|
101
101
|
const text = existing && 'text' in existing && typeof existing.text === 'string' ? existing.text : '';
|
|
102
|
-
upsertReasoning(state, reasoningItem(state.sessionId, id, { type: '
|
|
102
|
+
upsertReasoning(state, reasoningItem(state.sessionId, id, { type: 'thinking', text: text + delta }, timestamp));
|
|
103
103
|
return true;
|
|
104
104
|
}
|
|
105
105
|
if (part.type === 'tool-call' && typeof part.toolCallId === 'string') {
|
|
@@ -134,7 +134,7 @@ export function lightweightThreadReasoning(sessionId, runId, events) {
|
|
|
134
134
|
for (const [index, event] of events.entries()) {
|
|
135
135
|
const timestamp = typeof event.timestamp === 'number' ? event.timestamp : 0;
|
|
136
136
|
if ((event.type === 'thinking' || event.type === 'text') && typeof event.text === 'string') {
|
|
137
|
-
upsertReasoning(state, reasoningItem(sessionId, `${runId}:${event.type}:${event.sequenceIndex ?? index}`, { type: 'text', text: event.text }, timestamp));
|
|
137
|
+
upsertReasoning(state, reasoningItem(sessionId, `${runId}:${event.type}:${event.sequenceIndex ?? index}`, { type: event.type === 'thinking' ? 'thinking' : 'text', text: event.text }, timestamp));
|
|
138
138
|
}
|
|
139
139
|
else if (event.type === 'tool_call') {
|
|
140
140
|
applyLightweightPart(state, { type: 'tool-call', toolCallId: event.callId, toolName: event.toolName, input: event.arguments }, index, timestamp);
|
|
@@ -6,7 +6,6 @@ type MemberUser = {
|
|
|
6
6
|
export declare function useWorkspaceMembers(): {
|
|
7
7
|
members: any;
|
|
8
8
|
membersMap: any;
|
|
9
|
-
getMemberImage: (userId: string) => string | undefined;
|
|
10
9
|
getMember: (userId: string) => MemberUser | undefined;
|
|
11
10
|
getMemberByEmail: (email: string) => MemberUser | undefined;
|
|
12
11
|
};
|
|
@@ -19,9 +19,6 @@ export function useWorkspaceMembers() {
|
|
|
19
19
|
}
|
|
20
20
|
return map;
|
|
21
21
|
});
|
|
22
|
-
function getMemberImage(userId) {
|
|
23
|
-
return membersMap.value.get(userId)?.image ?? void 0;
|
|
24
|
-
}
|
|
25
22
|
function getMember(userId) {
|
|
26
23
|
return membersMap.value.get(userId);
|
|
27
24
|
}
|
|
@@ -31,7 +28,6 @@ export function useWorkspaceMembers() {
|
|
|
31
28
|
return {
|
|
32
29
|
members,
|
|
33
30
|
membersMap,
|
|
34
|
-
getMemberImage,
|
|
35
31
|
getMember,
|
|
36
32
|
getMemberByEmail
|
|
37
33
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@meistrari/chat-nuxt",
|
|
3
|
-
"version": "4.6.0-rc.
|
|
3
|
+
"version": "4.6.0-rc.11",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"exports": {
|
|
6
6
|
".": {
|
|
@@ -53,7 +53,7 @@
|
|
|
53
53
|
"@iconify/vue": "^5.0.0",
|
|
54
54
|
"@meistrari/auth-nuxt": "3.31.0",
|
|
55
55
|
"@meistrari/logger": "^2.1.3",
|
|
56
|
-
"@meistrari/tela-build": "^1.
|
|
56
|
+
"@meistrari/tela-build": "^1.79.0",
|
|
57
57
|
"@sentry/nuxt": "^10.0.0",
|
|
58
58
|
"@vueuse/components": "^12.8.0",
|
|
59
59
|
"@vueuse/core": "^12.8.0",
|