@meistrari/chat-nuxt 3.17.0-rc.4 → 3.17.0-rc.6

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.
Files changed (25) hide show
  1. package/dist/module.json +1 -1
  2. package/dist/runtime/citations/components/chat-url-citation.vue +18 -3
  3. package/dist/runtime/citations/site-name.d.ts +5 -0
  4. package/dist/runtime/citations/site-name.js +36 -0
  5. package/dist/runtime/embed/components/chat-embed-inner.d.vue.ts +5 -1
  6. package/dist/runtime/embed/components/chat-embed-inner.vue +10 -2
  7. package/dist/runtime/embed/components/chat-embed-inner.vue.d.ts +5 -1
  8. package/dist/runtime/embed/components/chat-embed.d.vue.ts +3 -1
  9. package/dist/runtime/embed/components/chat-embed.vue +3 -0
  10. package/dist/runtime/embed/components/chat-embed.vue.d.ts +3 -1
  11. package/dist/runtime/embed/components/meistrari-chat-embed.d.vue.ts +4 -1
  12. package/dist/runtime/embed/components/meistrari-chat-embed.vue +13 -2
  13. package/dist/runtime/embed/components/meistrari-chat-embed.vue.d.ts +4 -1
  14. package/dist/runtime/messages/components/chat-message-bubble.d.vue.ts +13 -1
  15. package/dist/runtime/messages/components/chat-message-bubble.vue +26 -19
  16. package/dist/runtime/messages/components/chat-message-bubble.vue.d.ts +13 -1
  17. package/dist/runtime/messages/components/chat-message-list.d.vue.ts +12 -1
  18. package/dist/runtime/messages/components/chat-message-list.vue +6 -1
  19. package/dist/runtime/messages/components/chat-message-list.vue.d.ts +12 -1
  20. package/dist/runtime/server/api/citation-metadata.get.d.ts +8 -0
  21. package/dist/runtime/server/api/citation-metadata.get.js +12 -0
  22. package/dist/runtime/server/utils/citation-metadata.d.ts +16 -0
  23. package/dist/runtime/server/utils/citation-metadata.js +187 -0
  24. package/dist/runtime/types/embed.d.ts +1 -1
  25. package/package.json +3 -1
package/dist/module.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@meistrari/chat-nuxt",
3
3
  "configKey": "chatNuxt",
4
- "version": "3.17.0-rc.4",
4
+ "version": "3.17.0-rc.6",
5
5
  "builder": {
6
6
  "@nuxt/module-builder": "1.0.2",
7
7
  "unbuild": "unknown"
@@ -1,9 +1,24 @@
1
1
  <script setup>
2
+ import { loadCitationSiteName } from "../site-name";
2
3
  const props = defineProps({
3
4
  citation: { type: Object, required: true }
4
5
  });
5
6
  const faviconFailed = ref(false);
6
- const accessibleLabel = computed(() => `${props.citation.sourceLabel}: ${props.citation.title}`);
7
+ const siteName = ref(null);
8
+ const sourceLabel = computed(() => siteName.value || props.citation.sourceLabel);
9
+ onMounted(() => {
10
+ watch(() => props.citation.href, async (href, _previous, onCleanup) => {
11
+ let active = true;
12
+ onCleanup(() => {
13
+ active = false;
14
+ });
15
+ siteName.value = null;
16
+ const name = await loadCitationSiteName(href);
17
+ if (active)
18
+ siteName.value = name;
19
+ }, { immediate: true });
20
+ });
21
+ const accessibleLabel = computed(() => `${sourceLabel.value}: ${props.citation.title}`);
7
22
  </script>
8
23
 
9
24
  <template>
@@ -32,7 +47,7 @@ const accessibleLabel = computed(() => `${props.citation.sourceLabel}: ${props.c
32
47
  @error="faviconFailed = true"
33
48
  >
34
49
  <span v-else i-ph-globe-simple class="url-citation-fallback" aria-hidden="true" />
35
- <span>{{ citation.sourceLabel }}</span>
50
+ <span>{{ sourceLabel }}</span>
36
51
  </a>
37
52
 
38
53
  <template #content>
@@ -48,7 +63,7 @@ const accessibleLabel = computed(() => `${props.citation.sourceLabel}: ${props.c
48
63
  >
49
64
  <span v-else i-ph-globe-simple class="url-citation-card__fallback" aria-hidden="true" />
50
65
  <span body-12-medium text-secondary truncate>
51
- {{ citation.sourceLabel }}
66
+ {{ sourceLabel }}
52
67
  </span>
53
68
  </div>
54
69
 
@@ -0,0 +1,5 @@
1
+ export declare function createCitationSiteNameLoader(fetchMetadata: (url: string) => Promise<{
2
+ siteName?: string | null;
3
+ retryable?: boolean;
4
+ }>, now?: () => number): (href: string) => Promise<string | null>;
5
+ export declare const loadCitationSiteName: (href: string) => Promise<string | null>;
@@ -0,0 +1,36 @@
1
+ export function createCitationSiteNameLoader(fetchMetadata, now = Date.now) {
2
+ const cache = /* @__PURE__ */ new Map();
3
+ return (href) => {
4
+ let key;
5
+ try {
6
+ const url = new URL(href);
7
+ url.hash = "";
8
+ key = url.href;
9
+ } catch {
10
+ return Promise.resolve(null);
11
+ }
12
+ const cached = cache.get(key);
13
+ if (cached && cached.expires > now())
14
+ return cached.result;
15
+ const result = Promise.resolve().then(() => fetchMetadata(key)).then((value) => {
16
+ if (value.retryable)
17
+ throw new Error("Citation metadata temporarily unavailable");
18
+ return typeof value.siteName === "string" && value.siteName.trim() ? value.siteName.trim() : null;
19
+ }).catch(() => {
20
+ if (cache.get(key)?.result === result)
21
+ cache.delete(key);
22
+ return null;
23
+ });
24
+ cache.delete(key);
25
+ if (cache.size >= 512)
26
+ cache.delete(cache.keys().next().value);
27
+ cache.set(key, { expires: now() + 60 * 60 * 1e3, result });
28
+ return result;
29
+ };
30
+ }
31
+ export const loadCitationSiteName = createCitationSiteNameLoader(async (url) => {
32
+ const response = await fetch(`/api/citation-metadata?${new URLSearchParams({ url })}`);
33
+ if (!response.ok)
34
+ throw new Error("Citation metadata unavailable");
35
+ return response.json();
36
+ });
@@ -15,9 +15,13 @@ type __VLS_Props = {
15
15
  user?: EmbedUser | null;
16
16
  features?: Partial<ChatFeatureConfig>;
17
17
  };
18
- declare var __VLS_11: {};
18
+ declare var __VLS_11: {}, __VLS_94: any, __VLS_200: any;
19
19
  type __VLS_Slots = {} & {
20
20
  'sidebar-bottom'?: (props: typeof __VLS_11) => any;
21
+ } & {
22
+ 'failed-message-actions'?: (props: typeof __VLS_94) => any;
23
+ } & {
24
+ 'failed-message-actions'?: (props: typeof __VLS_200) => any;
21
25
  };
22
26
  declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {} & {
23
27
  "update:conversationId": (value: string | null) => any;
@@ -1023,7 +1023,11 @@ async function handleDelete() {
1023
1023
  :show-ttft="resolvedFeatures.showTtft"
1024
1024
  variant="mobile"
1025
1025
  @retry="handleRetry"
1026
- />
1026
+ >
1027
+ <template v-if="$slots['failed-message-actions']" #failed-message-actions="slotProps">
1028
+ <slot name="failed-message-actions" v-bind="slotProps" />
1029
+ </template>
1030
+ </ChatMessageList>
1027
1031
  <ChatMessageInput
1028
1032
  ref="messageInputRef"
1029
1033
  v-model:selected-model="selectedModel"
@@ -1229,7 +1233,11 @@ async function handleDelete() {
1229
1233
  :show-ttft="resolvedFeatures.showTtft"
1230
1234
  :variant="compact ? 'widget' : 'page'"
1231
1235
  @retry="handleRetry"
1232
- />
1236
+ >
1237
+ <template v-if="$slots['failed-message-actions']" #failed-message-actions="slotProps">
1238
+ <slot name="failed-message-actions" v-bind="slotProps" />
1239
+ </template>
1240
+ </ChatMessageList>
1233
1241
  <ChatMessageInput
1234
1242
  ref="messageInputRef"
1235
1243
  v-model:selected-model="selectedModel"
@@ -15,9 +15,13 @@ type __VLS_Props = {
15
15
  user?: EmbedUser | null;
16
16
  features?: Partial<ChatFeatureConfig>;
17
17
  };
18
- declare var __VLS_11: {};
18
+ declare var __VLS_11: {}, __VLS_94: any, __VLS_200: any;
19
19
  type __VLS_Slots = {} & {
20
20
  'sidebar-bottom'?: (props: typeof __VLS_11) => any;
21
+ } & {
22
+ 'failed-message-actions'?: (props: typeof __VLS_94) => any;
23
+ } & {
24
+ 'failed-message-actions'?: (props: typeof __VLS_200) => any;
21
25
  };
22
26
  declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {} & {
23
27
  "update:conversationId": (value: string | null) => any;
@@ -2,9 +2,11 @@ import { useChatAuth } from '#chat-auth';
2
2
  import { type ChatActionPayload } from '../../messages/composables/chat-action.js';
3
3
  import { type MessageFeedbackPayload } from '../../messages/composables/chat-feedback.js';
4
4
  import { type ChatMessageTerminalPayload } from '../../messages/composables/chat-message-terminal.js';
5
- declare var __VLS_13: {};
5
+ declare var __VLS_13: {}, __VLS_16: any;
6
6
  type __VLS_Slots = {} & {
7
7
  'sidebar-bottom'?: (props: typeof __VLS_13) => any;
8
+ } & {
9
+ 'failed-message-actions'?: (props: typeof __VLS_16) => any;
8
10
  };
9
11
  declare const __VLS_base: import("vue").DefineComponent<(import("../types.js").ChatEmbedSharedProps & {
10
12
  telaAgentId?: undefined;
@@ -131,5 +131,8 @@ provideChatMessageTerminal((payload) => {
131
131
  <template v-if="$slots['sidebar-bottom']" #sidebar-bottom>
132
132
  <slot name="sidebar-bottom" />
133
133
  </template>
134
+ <template v-if="$slots['failed-message-actions']" #failed-message-actions="slotProps">
135
+ <slot name="failed-message-actions" v-bind="slotProps" />
136
+ </template>
134
137
  </ChatEmbedInner>
135
138
  </template>
@@ -2,9 +2,11 @@ import { useChatAuth } from '#chat-auth';
2
2
  import { type ChatActionPayload } from '../../messages/composables/chat-action.js';
3
3
  import { type MessageFeedbackPayload } from '../../messages/composables/chat-feedback.js';
4
4
  import { type ChatMessageTerminalPayload } from '../../messages/composables/chat-message-terminal.js';
5
- declare var __VLS_13: {};
5
+ declare var __VLS_13: {}, __VLS_16: any;
6
6
  type __VLS_Slots = {} & {
7
7
  'sidebar-bottom'?: (props: typeof __VLS_13) => any;
8
+ } & {
9
+ 'failed-message-actions'?: (props: typeof __VLS_16) => any;
8
10
  };
9
11
  declare const __VLS_base: import("vue").DefineComponent<(import("../types.js").ChatEmbedSharedProps & {
10
12
  telaAgentId?: undefined;
@@ -2,6 +2,7 @@ import type { DefineSetupFnComponent, SlotsType } from 'vue';
2
2
  import type { ChatActionPayload } from '../../messages/composables/chat-action.js';
3
3
  import type { MessageFeedbackPayload } from '../../messages/composables/chat-feedback.js';
4
4
  import type { ChatMessageTerminalPayload } from '../../messages/composables/chat-message-terminal.js';
5
+ import type { Message } from '../../types/schemas/chat/conversations.js';
5
6
  import type { ChatFeatureConfig } from '../feature-config.js';
6
7
  import type { AvailableChatModels, ChatEmbedVariant, ChatEmbedVariantProps, ChatSidebarConfig, ChatSuggestion, DefaultChatEmbedProps, MeistrariChatEmbedProps, TelaAgentChatEmbedProps } from '../types.js';
7
8
 
@@ -23,6 +24,8 @@ type MeistrariChatEmbedEmits = {
23
24
  type MeistrariChatEmbedSlots = SlotsType<{
24
25
  /** Rendered in a dedicated bottom pane of the conversation sidebar, splitting it in half. Page variant only. */
25
26
  'sidebar-bottom'?: () => any;
27
+ /** Rendered inside a failed assistant message's error box, next to "Tentar novamente"; receives the failed message. */
28
+ 'failed-message-actions'?: (props: { message: Message }) => any;
26
29
  }>;
27
30
 
28
31
  /**
@@ -54,4 +57,4 @@ type MeistrariChatEmbedSlots = SlotsType<{
54
57
  declare const MeistrariChatEmbed: DefineSetupFnComponent<MeistrariChatEmbedProps, MeistrariChatEmbedEmits, MeistrariChatEmbedSlots>;
55
58
 
56
59
  export default MeistrariChatEmbed;
57
- export type { AvailableChatModels, ChatActionPayload, ChatEmbedVariant, ChatEmbedVariantProps, ChatFeatureConfig, ChatMessageTerminalPayload, ChatSidebarConfig, ChatSuggestion, DefaultChatEmbedProps, MeistrariChatEmbedProps, MessageFeedbackPayload, TelaAgentChatEmbedProps };
60
+ export type { AvailableChatModels, ChatActionPayload, ChatEmbedVariant, ChatEmbedVariantProps, ChatFeatureConfig, ChatMessageTerminalPayload, ChatSidebarConfig, ChatSuggestion, DefaultChatEmbedProps, MeistrariChatEmbedProps, Message, MessageFeedbackPayload, TelaAgentChatEmbedProps };
@@ -109,6 +109,9 @@ const widgetEmbedProps = computed(() => ({
109
109
  <template v-if="$slots['sidebar-bottom']" #sidebar-bottom>
110
110
  <slot name="sidebar-bottom" />
111
111
  </template>
112
+ <template v-if="$slots['failed-message-actions']" #failed-message-actions="slotProps">
113
+ <slot name="failed-message-actions" v-bind="slotProps" />
114
+ </template>
112
115
  </ChatEmbed>
113
116
 
114
117
  <ClientOnly v-else>
@@ -138,7 +141,11 @@ const widgetEmbedProps = computed(() => ({
138
141
  @action="emit('action', $event)"
139
142
  @message-feedback="emit('messageFeedback', $event)"
140
143
  @message-terminal="emit('messageTerminal', $event)"
141
- />
144
+ >
145
+ <template v-if="$slots['failed-message-actions']" #failed-message-actions="slotProps">
146
+ <slot name="failed-message-actions" v-bind="slotProps" />
147
+ </template>
148
+ </ChatEmbed>
142
149
  </div>
143
150
  </TelaChatFloatingWindow>
144
151
 
@@ -163,7 +170,11 @@ const widgetEmbedProps = computed(() => ({
163
170
  @action="emit('action', $event)"
164
171
  @message-feedback="emit('messageFeedback', $event)"
165
172
  @message-terminal="emit('messageTerminal', $event)"
166
- />
173
+ >
174
+ <template v-if="$slots['failed-message-actions']" #failed-message-actions="slotProps">
175
+ <slot name="failed-message-actions" v-bind="slotProps" />
176
+ </template>
177
+ </ChatEmbed>
167
178
  </div>
168
179
  </TelaChatSidepanelWindow>
169
180
  </ClientOnly>
@@ -2,6 +2,7 @@ import type { DefineSetupFnComponent, SlotsType } from 'vue';
2
2
  import type { ChatActionPayload } from '../../messages/composables/chat-action.js';
3
3
  import type { MessageFeedbackPayload } from '../../messages/composables/chat-feedback.js';
4
4
  import type { ChatMessageTerminalPayload } from '../../messages/composables/chat-message-terminal.js';
5
+ import type { Message } from '../../types/schemas/chat/conversations.js';
5
6
  import type { ChatFeatureConfig } from '../feature-config.js';
6
7
  import type { AvailableChatModels, ChatEmbedVariant, ChatEmbedVariantProps, ChatSidebarConfig, ChatSuggestion, DefaultChatEmbedProps, MeistrariChatEmbedProps, TelaAgentChatEmbedProps } from '../types.js';
7
8
 
@@ -23,6 +24,8 @@ type MeistrariChatEmbedEmits = {
23
24
  type MeistrariChatEmbedSlots = SlotsType<{
24
25
  /** Rendered in a dedicated bottom pane of the conversation sidebar, splitting it in half. Page variant only. */
25
26
  'sidebar-bottom'?: () => any;
27
+ /** Rendered inside a failed assistant message's error box, next to "Tentar novamente"; receives the failed message. */
28
+ 'failed-message-actions'?: (props: { message: Message }) => any;
26
29
  }>;
27
30
 
28
31
  /**
@@ -54,4 +57,4 @@ type MeistrariChatEmbedSlots = SlotsType<{
54
57
  declare const MeistrariChatEmbed: DefineSetupFnComponent<MeistrariChatEmbedProps, MeistrariChatEmbedEmits, MeistrariChatEmbedSlots>;
55
58
 
56
59
  export default MeistrariChatEmbed;
57
- export type { AvailableChatModels, ChatActionPayload, ChatEmbedVariant, ChatEmbedVariantProps, ChatFeatureConfig, ChatMessageTerminalPayload, ChatSidebarConfig, ChatSuggestion, DefaultChatEmbedProps, MeistrariChatEmbedProps, MessageFeedbackPayload, TelaAgentChatEmbedProps };
60
+ export type { AvailableChatModels, ChatActionPayload, ChatEmbedVariant, ChatEmbedVariantProps, ChatFeatureConfig, ChatMessageTerminalPayload, ChatSidebarConfig, ChatSuggestion, DefaultChatEmbedProps, MeistrariChatEmbedProps, Message, MessageFeedbackPayload, TelaAgentChatEmbedProps };
@@ -9,12 +9,24 @@ type __VLS_Props = {
9
9
  preparingSteps?: readonly string[] | null;
10
10
  showTtft?: boolean;
11
11
  };
12
- declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {} & {
12
+ type __VLS_Slots = {
13
+ /** Host actions rendered inside a failed assistant message, next to the retry button. */
14
+ 'failed-message-actions'?: (props: {
15
+ message: Message;
16
+ }) => unknown;
17
+ };
18
+ declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {} & {
13
19
  retry: (messageId: string) => any;
14
20
  }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
15
21
  onRetry?: ((messageId: string) => any) | undefined;
16
22
  }>, {
17
23
  showTtft: boolean;
18
24
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
25
+ declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
19
26
  declare const _default: typeof __VLS_export;
20
27
  export default _default;
28
+ type __VLS_WithSlots<T, S> = T & {
29
+ new (): {
30
+ $slots: S;
31
+ };
32
+ };
@@ -13,6 +13,7 @@ const props = defineProps({
13
13
  showTtft: { type: Boolean, required: false, default: true }
14
14
  });
15
15
  const emit = defineEmits(["retry"]);
16
+ defineSlots();
16
17
  const { user } = useChatAuth();
17
18
  const { getMemberByEmail } = useWorkspaceMembers();
18
19
  const embedConfig = useEmbedConfig();
@@ -203,9 +204,14 @@ const hasDetectedFiles = computed(() => contentSegments.value.some((s) => s.type
203
204
  <span heading-h5-semibold text-primary>{{ isPromptTooLong ? "Limite de contexto atingido" : "Erro" }}</span>
204
205
  <span body-10-regular text-secondary>{{ formatTime(message.createdAt) }}</span>
205
206
  </div>
206
- <div v-if="isPromptTooLong" body-12-regular text-secondary whitespace-pre-wrap>
207
- Esta conversa atingiu o limite de contexto do modelo e não pode continuar. Inicie uma nova conversa para seguir com o assunto.
208
- </div>
207
+ <template v-if="isPromptTooLong">
208
+ <div body-12-regular text-secondary whitespace-pre-wrap>
209
+ Esta conversa atingiu o limite de contexto do modelo e não pode continuar. Inicie uma nova conversa para seguir com o assunto.
210
+ </div>
211
+ <div v-if="$slots['failed-message-actions']" mt-8px flex flex-wrap items-center gap-8px>
212
+ <slot name="failed-message-actions" :message="message" />
213
+ </div>
214
+ </template>
209
215
  <template v-else>
210
216
  <div body-12-regular text-secondary whitespace-pre-wrap>
211
217
  {{ displayContent }}
@@ -213,22 +219,23 @@ const hasDetectedFiles = computed(() => contentSegments.value.some((s) => s.type
213
219
  <div v-if="message.failReason" body-10-regular text-tertiary whitespace-pre-wrap>
214
220
  {{ message.failReason }}
215
221
  </div>
216
- <button
217
- mt-8px
218
- flex items-center gap-6px
219
- px-12px py-6px
220
- rounded-6px
221
- bg-gray-100
222
- hover:bg-gray-200
223
- text-13px text-gray-700
224
- transition-colors
225
- cursor-pointer
226
- self-start
227
- @click="emit('retry', message.id)"
228
- >
229
- <div i-ph-arrow-clockwise text-14px />
230
- <span>Tentar novamente</span>
231
- </button>
222
+ <div mt-8px flex flex-wrap items-center gap-8px>
223
+ <button
224
+ flex items-center gap-6px
225
+ px-12px py-6px
226
+ rounded-6px
227
+ bg-gray-100
228
+ hover:bg-gray-200
229
+ text-13px text-gray-700
230
+ transition-colors
231
+ cursor-pointer
232
+ @click="emit('retry', message.id)"
233
+ >
234
+ <div i-ph-arrow-clockwise text-14px />
235
+ <span>Tentar novamente</span>
236
+ </button>
237
+ <slot name="failed-message-actions" :message="message" />
238
+ </div>
232
239
  </template>
233
240
  </div>
234
241
  </template>
@@ -9,12 +9,24 @@ type __VLS_Props = {
9
9
  preparingSteps?: readonly string[] | null;
10
10
  showTtft?: boolean;
11
11
  };
12
- declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {} & {
12
+ type __VLS_Slots = {
13
+ /** Host actions rendered inside a failed assistant message, next to the retry button. */
14
+ 'failed-message-actions'?: (props: {
15
+ message: Message;
16
+ }) => unknown;
17
+ };
18
+ declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {} & {
13
19
  retry: (messageId: string) => any;
14
20
  }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
15
21
  onRetry?: ((messageId: string) => any) | undefined;
16
22
  }>, {
17
23
  showTtft: boolean;
18
24
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
25
+ declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
19
26
  declare const _default: typeof __VLS_export;
20
27
  export default _default;
28
+ type __VLS_WithSlots<T, S> = T & {
29
+ new (): {
30
+ $slots: S;
31
+ };
32
+ };
@@ -13,7 +13,12 @@ type __VLS_Props = {
13
13
  preparingSteps?: readonly string[] | null;
14
14
  showTtft?: boolean;
15
15
  };
16
- declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {
16
+ type __VLS_Slots = {
17
+ 'failed-message-actions'?: (props: {
18
+ message: Message;
19
+ }) => unknown;
20
+ };
21
+ declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {
17
22
  isPinned: any;
18
23
  scrollToBottom: any;
19
24
  }, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {} & {
@@ -23,5 +28,11 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {
23
28
  }>, {
24
29
  showTtft: boolean;
25
30
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
31
+ declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
26
32
  declare const _default: typeof __VLS_export;
27
33
  export default _default;
34
+ type __VLS_WithSlots<T, S> = T & {
35
+ new (): {
36
+ $slots: S;
37
+ };
38
+ };
@@ -11,6 +11,7 @@ const props = defineProps({
11
11
  showTtft: { type: Boolean, required: false, default: true }
12
12
  });
13
13
  const emit = defineEmits(["retry"]);
14
+ defineSlots();
14
15
  const conversationRef = ref(null);
15
16
  const conversationElements = computed(() => {
16
17
  const root = conversationRef.value?.$el;
@@ -106,7 +107,11 @@ defineExpose({
106
107
  :preparing-steps="item.value.role === 'assistant' && item.value.status === 'pending' ? preparingSteps : null"
107
108
  :show-ttft="showTtft"
108
109
  @retry="emit('retry', $event)"
109
- />
110
+ >
111
+ <template v-if="$slots['failed-message-actions']" #failed-message-actions="slotProps">
112
+ <slot name="failed-message-actions" v-bind="slotProps" />
113
+ </template>
114
+ </ChatMessageBubble>
110
115
  </template>
111
116
  <p
112
117
  v-if="messages.length > 0"
@@ -13,7 +13,12 @@ type __VLS_Props = {
13
13
  preparingSteps?: readonly string[] | null;
14
14
  showTtft?: boolean;
15
15
  };
16
- declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {
16
+ type __VLS_Slots = {
17
+ 'failed-message-actions'?: (props: {
18
+ message: Message;
19
+ }) => unknown;
20
+ };
21
+ declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {
17
22
  isPinned: any;
18
23
  scrollToBottom: any;
19
24
  }, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {} & {
@@ -23,5 +28,11 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {
23
28
  }>, {
24
29
  showTtft: boolean;
25
30
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
31
+ declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
26
32
  declare const _default: typeof __VLS_export;
27
33
  export default _default;
34
+ type __VLS_WithSlots<T, S> = T & {
35
+ new (): {
36
+ $slots: S;
37
+ };
38
+ };
@@ -0,0 +1,8 @@
1
+ declare const _default: import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<{
2
+ siteName: string | null;
3
+ retryable?: undefined;
4
+ } | {
5
+ siteName: null;
6
+ retryable: boolean;
7
+ }>>;
8
+ export default _default;
@@ -0,0 +1,12 @@
1
+ import { defineEventHandler, getQuery, setResponseHeaders } from "h3";
2
+ import { createSiteNameCache } from "../utils/citation-metadata.js";
3
+ const getSiteName = createSiteNameCache();
4
+ export default defineEventHandler(async (event) => {
5
+ setResponseHeaders(event, { "cache-control": "private, no-store" });
6
+ const url = getQuery(event).url;
7
+ try {
8
+ return { siteName: typeof url === "string" ? await getSiteName(url) : null };
9
+ } catch {
10
+ return { siteName: null, retryable: true };
11
+ }
12
+ });
@@ -0,0 +1,16 @@
1
+ export declare const MAX_HTML_BYTES: number;
2
+ export declare function publicCitationUrl(value: unknown): URL;
3
+ export declare function isPublicAddress(address: string): boolean;
4
+ export declare function parseCitationSiteName(html: string): string | null;
5
+ interface Address {
6
+ address: string;
7
+ family: number;
8
+ }
9
+ export declare function resolvePublicAddress(hostname: string, resolve: (host: string) => Promise<Address[]>): Promise<Address>;
10
+ declare function requestPage(url: URL, signal: AbortSignal): Promise<{
11
+ redirect?: string;
12
+ html?: string;
13
+ }>;
14
+ export declare function fetchCitationSiteName(input: string, request?: typeof requestPage): Promise<string | null>;
15
+ export declare function createSiteNameCache(load?: typeof fetchCitationSiteName, now?: () => number): (input: string) => Promise<string | null>;
16
+ export {};
@@ -0,0 +1,187 @@
1
+ import { Parser } from "htmlparser2";
2
+ import ipaddr from "ipaddr.js";
3
+ export const MAX_HTML_BYTES = 256 * 1024;
4
+ const TIMEOUT_MS = 5e3;
5
+ const MAX_REDIRECTS = 3;
6
+ class PermanentMetadataError extends Error {
7
+ }
8
+ export function publicCitationUrl(value) {
9
+ if (typeof value !== "string" || value.length > 2048)
10
+ throw new PermanentMetadataError("Invalid citation URL");
11
+ let url;
12
+ try {
13
+ url = new URL(value);
14
+ } catch {
15
+ throw new PermanentMetadataError("Invalid citation URL");
16
+ }
17
+ if (!["http:", "https:"].includes(url.protocol) || url.username || url.password || url.port)
18
+ throw new PermanentMetadataError("Invalid citation URL");
19
+ url.hash = "";
20
+ return url;
21
+ }
22
+ export function isPublicAddress(address) {
23
+ try {
24
+ return ipaddr.process(address.replace(/^\[|\]$/g, "")).range() === "unicast";
25
+ } catch {
26
+ return false;
27
+ }
28
+ }
29
+ export function parseCitationSiteName(html) {
30
+ let siteName = null;
31
+ const parser = new Parser({
32
+ onopentag(name, attributes) {
33
+ if (name !== "meta" || siteName || (attributes.property || attributes.name || "").toLowerCase() !== "og:site_name")
34
+ return;
35
+ const value = (attributes.content || "").replace(/[\p{Cc}\p{Cf}]/gu, " ").replace(/\s+/g, " ").trim();
36
+ if (value && value.length <= 100)
37
+ siteName = value;
38
+ }
39
+ }, { decodeEntities: true });
40
+ parser.end(html);
41
+ return siteName;
42
+ }
43
+ export async function resolvePublicAddress(hostname, resolve) {
44
+ const host = hostname.replace(/^\[|\]$/g, "");
45
+ const addresses = ipaddr.isValid(host) ? [{ address: host, family: ipaddr.parse(host).kind() === "ipv4" ? 4 : 6 }] : await resolve(host);
46
+ if (!addresses.length || addresses.some((record) => !isPublicAddress(record.address)))
47
+ throw new PermanentMetadataError("Non-public citation address");
48
+ return addresses[0];
49
+ }
50
+ async function requestPage(url, signal) {
51
+ const [{ lookup }, http, https, { Buffer }] = await Promise.all([
52
+ import("node:dns/promises"),
53
+ import("node:http"),
54
+ import("node:https"),
55
+ import("node:buffer")
56
+ ]);
57
+ const address = await resolvePublicAddress(url.hostname, (host) => lookup(host, { all: true }));
58
+ signal.throwIfAborted();
59
+ return new Promise((resolve, reject) => {
60
+ const request = (url.protocol === "https:" ? https.request : http.request)(url, {
61
+ agent: false,
62
+ family: address.family,
63
+ signal,
64
+ // Preserve the original Host header and TLS server name. Only the
65
+ // socket destination is pinned; no cookies or auth are forwarded.
66
+ lookup: (_hostname, _options, callback) => callback(null, address.address, address.family),
67
+ headers: { "accept": "text/html,application/xhtml+xml", "accept-encoding": "identity" }
68
+ }, (response) => {
69
+ const status = response.statusCode || 0;
70
+ if ([301, 302, 303, 307, 308].includes(status)) {
71
+ response.destroy();
72
+ resolve({ redirect: response.headers.location });
73
+ return;
74
+ }
75
+ if (status < 200 || status >= 300) {
76
+ response.destroy();
77
+ const retryable = status >= 500 || [408, 425, 429].includes(status);
78
+ reject(retryable ? new Error("Citation page unavailable") : new PermanentMetadataError("Citation page unavailable"));
79
+ return;
80
+ }
81
+ if (!/^(?:text\/html|application\/xhtml\+xml)(?:;|$)/i.test(response.headers["content-type"] || "") || response.headers["content-encoding"] && response.headers["content-encoding"] !== "identity") {
82
+ response.destroy();
83
+ reject(new PermanentMetadataError("Unsupported citation content"));
84
+ return;
85
+ }
86
+ const chunks = [];
87
+ let size = 0;
88
+ response.on("data", (chunk) => {
89
+ size += chunk.length;
90
+ if (size > MAX_HTML_BYTES) {
91
+ const remaining = MAX_HTML_BYTES - (size - chunk.length);
92
+ chunks.push(chunk.subarray(0, remaining));
93
+ resolve({ html: Buffer.concat(chunks).toString("utf8") });
94
+ response.destroy();
95
+ return;
96
+ }
97
+ chunks.push(chunk);
98
+ });
99
+ response.on("end", () => resolve({ html: Buffer.concat(chunks).toString("utf8") }));
100
+ response.on("error", reject);
101
+ });
102
+ request.on("error", reject);
103
+ request.end();
104
+ });
105
+ }
106
+ export async function fetchCitationSiteName(input, request = requestPage) {
107
+ const controller = new AbortController();
108
+ let timer;
109
+ const timeout = new Promise((_resolve, reject) => {
110
+ timer = setTimeout(() => {
111
+ controller.abort();
112
+ reject(new Error("Citation metadata timed out"));
113
+ }, TIMEOUT_MS);
114
+ });
115
+ try {
116
+ return await Promise.race([timeout, (async () => {
117
+ let url = publicCitationUrl(input);
118
+ for (let hop = 0; hop <= MAX_REDIRECTS; hop++) {
119
+ const response = await request(url, controller.signal);
120
+ if (!response.redirect)
121
+ return response.html ? parseCitationSiteName(response.html) : null;
122
+ let target;
123
+ try {
124
+ target = new URL(response.redirect, url);
125
+ } catch {
126
+ throw new PermanentMetadataError("Invalid citation redirect");
127
+ }
128
+ url = publicCitationUrl(target.href);
129
+ }
130
+ throw new PermanentMetadataError("Too many citation redirects");
131
+ })()]);
132
+ } catch (error) {
133
+ if (error instanceof PermanentMetadataError)
134
+ return null;
135
+ throw error;
136
+ } finally {
137
+ clearTimeout(timer);
138
+ controller.abort();
139
+ }
140
+ }
141
+ export function createSiteNameCache(load = fetchCitationSiteName, now = Date.now) {
142
+ const cache = /* @__PURE__ */ new Map();
143
+ const queue = [];
144
+ let active = 0;
145
+ return (input) => {
146
+ let key;
147
+ try {
148
+ key = publicCitationUrl(input).href;
149
+ } catch {
150
+ return Promise.resolve(null);
151
+ }
152
+ const cached = cache.get(key);
153
+ if (cached && cached.expires > now())
154
+ return cached.result;
155
+ if (active >= 8 && queue.length >= 64)
156
+ return Promise.reject(new Error("Citation metadata queue full"));
157
+ const entry = { expires: Number.POSITIVE_INFINITY, result: Promise.resolve(null) };
158
+ entry.result = new Promise((resolve, reject) => {
159
+ const start = () => {
160
+ active++;
161
+ void Promise.resolve().then(() => load(key)).then((name) => {
162
+ entry.expires = now() + 60 * 60 * 1e3;
163
+ resolve(name);
164
+ }, (error) => {
165
+ if (cache.get(key) === entry)
166
+ cache.delete(key);
167
+ reject(error);
168
+ }).finally(() => {
169
+ active--;
170
+ queue.shift()?.();
171
+ });
172
+ };
173
+ if (active < 8)
174
+ start();
175
+ else
176
+ queue.push(start);
177
+ });
178
+ cache.delete(key);
179
+ if (cache.size >= 512) {
180
+ const oldestCompleted = [...cache].find(([, value]) => Number.isFinite(value.expires));
181
+ if (oldestCompleted)
182
+ cache.delete(oldestCompleted[0]);
183
+ }
184
+ cache.set(key, entry);
185
+ return entry.result;
186
+ };
187
+ }
@@ -5,7 +5,7 @@ export type { ChatActionPayload } from '../messages/composables/chat-action.js';
5
5
  export type { MessageFeedbackConfig, MessageFeedbackPayload, MessageFeedbackRating, MessageFeedbackSubmitHandler, MessageFeedbackSubmitResult, } from '../messages/composables/chat-feedback.js';
6
6
  export { ChatMessageStatus } from '../messages/composables/chat-message-terminal.js';
7
7
  export type { ChatMessageTerminalPayload } from '../messages/composables/chat-message-terminal.js';
8
- export type { ConversationEvent } from './schemas/chat/conversations.js';
8
+ export type { ConversationEvent, Message } from './schemas/chat/conversations.js';
9
9
  export type { MessageOverviewResponse } from './schemas/chat/messages.js';
10
10
  export type { ConversationCreatorFilter } from './schemas/chat/primitives.js';
11
11
  export type { TelaAgentExecutionInput } from './schemas/integrations/tela-agent-api.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@meistrari/chat-nuxt",
3
- "version": "3.17.0-rc.4",
3
+ "version": "3.17.0-rc.6",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": {
@@ -51,6 +51,8 @@
51
51
  "@vueuse/components": "^12.8.0",
52
52
  "@vueuse/core": "^12.8.0",
53
53
  "h3": "^1.15.0",
54
+ "htmlparser2": "12.0.0",
55
+ "ipaddr.js": "2.5.0",
54
56
  "markstream-vue": "2.0.7",
55
57
  "mermaid": "^11.13.0",
56
58
  "motion": "^11.13.0",