@meistrari/chat-nuxt 3.6.0 → 3.8.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.
Files changed (42) hide show
  1. package/README.md +26 -0
  2. package/dist/module.d.mts +2 -0
  3. package/dist/module.json +1 -1
  4. package/dist/module.mjs +6 -3
  5. package/dist/runtime/citations/citation-links.d.ts +32 -0
  6. package/dist/runtime/citations/citation-links.js +56 -0
  7. package/dist/runtime/citations/components/chat-citation.d.vue.ts +9 -0
  8. package/dist/runtime/citations/components/chat-citation.vue +186 -0
  9. package/dist/runtime/citations/components/chat-citation.vue.d.ts +9 -0
  10. package/dist/runtime/citations/components/citation-document-panel.d.vue.ts +3 -0
  11. package/dist/runtime/citations/components/citation-document-panel.vue +217 -0
  12. package/dist/runtime/citations/components/citation-document-panel.vue.d.ts +3 -0
  13. package/dist/runtime/citations/composables/citation-panel.d.ts +8 -0
  14. package/dist/runtime/citations/composables/citation-panel.js +17 -0
  15. package/dist/runtime/citations/composables/citations.d.ts +17 -0
  16. package/dist/runtime/citations/composables/citations.js +44 -0
  17. package/dist/runtime/citations/document-highlight.d.ts +13 -0
  18. package/dist/runtime/citations/document-highlight.js +35 -0
  19. package/dist/runtime/citations/types.d.ts +57 -0
  20. package/dist/runtime/citations/types.js +0 -0
  21. package/dist/runtime/conversations/components/chat-conversation-list.vue +2 -2
  22. package/dist/runtime/conversations/composables/conversations.js +5 -0
  23. package/dist/runtime/embed/components/chat-embed-inner.vue +53 -2
  24. package/dist/runtime/embed/components/chat-embed.vue +2 -0
  25. package/dist/runtime/embed/components/meistrari-chat-embed.vue +3 -0
  26. package/dist/runtime/embed/components/mobile/chat-mobile-drawer.vue +2 -2
  27. package/dist/runtime/embed/composables/embed-config.d.ts +3 -0
  28. package/dist/runtime/embed/composables/embed-config.js +9 -0
  29. package/dist/runtime/embed/types.d.ts +12 -1
  30. package/dist/runtime/files/components/chat-vault-link.vue +13 -0
  31. package/dist/runtime/files/content-detection.d.ts +9 -1
  32. package/dist/runtime/files/content-detection.js +29 -8
  33. package/dist/runtime/messages/components/chat-message-bubble.vue +7 -3
  34. package/dist/runtime/server/utils/chat-api-proxy.js +10 -1
  35. package/dist/runtime/shared/composables/feature-flags.d.ts +8 -1
  36. package/dist/runtime/shared/composables/feature-flags.js +10 -1
  37. package/dist/runtime/types/citations.d.ts +1 -0
  38. package/dist/runtime/types/citations.js +0 -0
  39. package/dist/runtime/types/embed.d.ts +1 -0
  40. package/dist/runtime/types/schemas/chat/usage.d.ts +2 -0
  41. package/dist/runtime/types/schemas/chat/usage.js +1 -0
  42. package/package.json +1 -1
@@ -0,0 +1,44 @@
1
+ import { citationCacheKey } from "../citation-links.js";
2
+ export function useCitations() {
3
+ const nuxtApp = useNuxtApp();
4
+ const caches = nuxtApp._chatCitationEntries ??= /* @__PURE__ */ new WeakMap();
5
+ function entriesFor(config) {
6
+ let entries = caches.get(config);
7
+ if (!entries) {
8
+ entries = /* @__PURE__ */ new Map();
9
+ caches.set(config, entries);
10
+ }
11
+ return entries;
12
+ }
13
+ function resolveInto(entry, config, citation) {
14
+ entry.status.value = "loading";
15
+ config.resolve(citation).then((meta) => {
16
+ entry.meta.value = meta;
17
+ entry.status.value = meta ? "resolved" : "unresolved";
18
+ }).catch(() => {
19
+ entry.status.value = "unresolved";
20
+ });
21
+ }
22
+ function resolveCitation(config, citation) {
23
+ const entries = entriesFor(config);
24
+ const key = citationCacheKey(citation);
25
+ const existing = entries.get(key);
26
+ if (existing)
27
+ return existing;
28
+ const entry = {
29
+ status: ref("loading"),
30
+ meta: ref(null)
31
+ };
32
+ entries.set(key, entry);
33
+ resolveInto(entry, config, citation);
34
+ return entry;
35
+ }
36
+ function retryCitation(config, citation) {
37
+ const existing = entriesFor(config).get(citationCacheKey(citation));
38
+ if (!existing)
39
+ return resolveCitation(config, citation);
40
+ resolveInto(existing, config, citation);
41
+ return existing;
42
+ }
43
+ return { resolveCitation, retryCitation };
44
+ }
@@ -0,0 +1,13 @@
1
+ import type { CitationOccurrence } from './types.js';
2
+ export type HighlightSegment = {
3
+ text: string;
4
+ /** 1-based occurrence number when this segment is a highlighted match. */
5
+ occurrence?: number;
6
+ };
7
+ /**
8
+ * Fallback when the host provides no `locate` callback. Plain case-insensitive matching:
9
+ * accent variants won't match, which degrades to "term not found" rather than breaking.
10
+ */
11
+ export declare function locateTermClientSide(content: string, term: string): CitationOccurrence[];
12
+ /** Occurrence ranges are host data: invalid, out-of-bounds and overlapping ranges are dropped. */
13
+ export declare function splitContentByOccurrences(content: string, occurrences: CitationOccurrence[]): HighlightSegment[];
@@ -0,0 +1,35 @@
1
+ export function locateTermClientSide(content, term) {
2
+ const needle = term.toLowerCase();
3
+ if (!needle)
4
+ return [];
5
+ const haystack = content.toLowerCase();
6
+ const occurrences = [];
7
+ let cursor = 0;
8
+ while (true) {
9
+ const index = haystack.indexOf(needle, cursor);
10
+ if (index === -1)
11
+ break;
12
+ occurrences.push({ offset: index, end: index + needle.length });
13
+ cursor = index + needle.length;
14
+ }
15
+ return occurrences;
16
+ }
17
+ export function splitContentByOccurrences(content, occurrences) {
18
+ const ordered = [...occurrences].filter((occurrence) => Number.isInteger(occurrence.offset) && Number.isInteger(occurrence.end) && occurrence.offset >= 0 && occurrence.end > occurrence.offset && occurrence.offset < content.length).sort((a, b) => a.offset - b.offset);
19
+ const segments = [];
20
+ let lastEnd = 0;
21
+ let count = 0;
22
+ for (const occurrence of ordered) {
23
+ if (occurrence.offset < lastEnd)
24
+ continue;
25
+ const end = Math.min(occurrence.end, content.length);
26
+ if (occurrence.offset > lastEnd)
27
+ segments.push({ text: content.slice(lastEnd, occurrence.offset) });
28
+ count += 1;
29
+ segments.push({ text: content.slice(occurrence.offset, end), occurrence: count });
30
+ lastEnd = end;
31
+ }
32
+ if (lastEnd < content.length)
33
+ segments.push({ text: content.slice(lastEnd) });
34
+ return segments.length > 0 ? segments : [{ text: content }];
35
+ }
@@ -0,0 +1,57 @@
1
+ import type { Component } from 'vue';
2
+ import type { CitationRef } from './citation-links.js';
3
+ export type { CitationLocator, CitationRef } from './citation-links.js';
4
+ export type CitationMeta = {
5
+ /** Human-readable document title. Hosts must never surface internal ids here. */
6
+ title: string;
7
+ /** Display name of the base/source the document belongs to. */
8
+ sourceLabel?: string;
9
+ /** Identifier of the document at its origin (case number, súmula number, ...). */
10
+ identifier?: string;
11
+ /** Link to the document at its original source, when one exists. */
12
+ url?: string;
13
+ /** ISO date associated with the document. */
14
+ date?: string;
15
+ /** What `date` refers to (julgamento, aprovação, publicação, ...). */
16
+ dateLabel?: string;
17
+ /** Short excerpt shown in the citation popover. */
18
+ snippet?: string;
19
+ /** Status badge, e.g. a súmula that is no longer in force. */
20
+ badge?: {
21
+ label: string;
22
+ tone?: 'neutral' | 'warning' | 'danger';
23
+ };
24
+ };
25
+ export type CitationDocument = {
26
+ /** Plain-text document content as stored by the host. */
27
+ content: string;
28
+ /** True when the host cut `content` short; the panel discloses it. */
29
+ truncated?: boolean;
30
+ };
31
+ export type CitationOccurrence = {
32
+ /** Inclusive char offset of the occurrence in the document content. */
33
+ offset: number;
34
+ /** Exclusive end offset of the occurrence. */
35
+ end: number;
36
+ };
37
+ /**
38
+ * Host-provided citation behavior. The lib owns detection, rendering and interaction
39
+ * states; the host owns data access (and with it, authorization: every callback runs
40
+ * against the host's own backend with the viewing user's identity).
41
+ *
42
+ * A `resolve` that returns `null` renders the citation as unverified — deliberately
43
+ * indistinguishable between "does not exist" and "not visible to this viewer".
44
+ */
45
+ export type ChatCitationsConfig = {
46
+ /** Resolve a citation to display metadata; `null` marks it unverified. */
47
+ resolve: (ref: CitationRef) => Promise<CitationMeta | null>;
48
+ /** Fetch the document body; providing this enables the built-in document panel. */
49
+ getDocument?: (ref: CitationRef) => Promise<CitationDocument | null>;
50
+ /** Locate a term's occurrences for highlighting; absent = client-side best effort. */
51
+ locate?: (ref: CitationRef, term: string) => Promise<CitationOccurrence[] | null>;
52
+ /** Replaces the built-in citation chip. */
53
+ component?: Component;
54
+ /** Takes over the click entirely; the built-in panel is not opened. */
55
+ onOpen?: (ref: CitationRef, meta: CitationMeta | null) => void;
56
+ };
57
+ export type CitationStatus = 'loading' | 'resolved' | 'unresolved';
File without changes
@@ -84,7 +84,7 @@ const {
84
84
 
85
85
  <template v-else>
86
86
  <div
87
- v-if="showGroupCreationPrompt"
87
+ v-if="showGroupCreationPrompt && !hideFolderHint"
88
88
  mx-8px
89
89
  flex="~ col" gap-10px
90
90
  >
@@ -113,7 +113,7 @@ const {
113
113
  <span h-10px w-84px rounded-full bg-neutral-100 />
114
114
  </div>
115
115
  </div>
116
- <p v-if="!hideFolderHint" px-4px text-14px text-neutral-500 leading-20px>
116
+ <p px-4px text-14px text-neutral-500 leading-20px>
117
117
  Suas pastas aparecem aqui. Crie uma para começar a agrupar conversas.
118
118
  </p>
119
119
  <ChatConversationNewGroupButton label="Criar pasta" variant="empty-state" />
@@ -293,6 +293,7 @@ export function useConversations() {
293
293
  };
294
294
  const createConversation = async (input) => {
295
295
  error.value = null;
296
+ const statusToast = useStatusToast();
296
297
  const requestRuntimeScope = runtimeScope.value;
297
298
  const metadataSnapshot = snapshotConversationMetadata(requestRuntimeScope);
298
299
  const now = /* @__PURE__ */ new Date();
@@ -333,6 +334,10 @@ export function useConversations() {
333
334
  restoreConversationMetadata(requestRuntimeScope, metadataSnapshot);
334
335
  const message = err instanceof Error ? err.message : "Erro ao criar conversa";
335
336
  error.value = message;
337
+ statusToast.update({
338
+ text: "Erro ao criar conversa",
339
+ icon: "i-ph-warning"
340
+ });
336
341
  return null;
337
342
  }
338
343
  };
@@ -81,6 +81,7 @@ const effectiveWorkspaceSettings = computed(() => {
81
81
  });
82
82
  const { closePanel: closeFilesPanel } = useConversationFilesPanel();
83
83
  const { isOpen: isPreviewPanelOpen, panelWidth: previewPanelWidth, currentFile: currentPreviewFile, closePreview } = useFilePreviewPanel();
84
+ const { isOpen: isCitationPanelOpen } = useCitationPanel();
84
85
  const activeConversationId = ref(
85
86
  normalizeConversationId(props.conversationId) ?? normalizeConversationId(props.initialConversationId)
86
87
  );
@@ -398,6 +399,32 @@ const editTitle = ref("");
398
399
  const justStartedEditing = ref(false);
399
400
  const showDebugModal = ref(false);
400
401
  const debugSessionId = computed(() => currentConversation.value?.threadSessionId ?? "Sess\xE3o ainda n\xE3o criada");
402
+ async function copyDebugInfo() {
403
+ const conversation = currentConversation.value;
404
+ const conversationId = conversation?.id ?? activeConversationId.value;
405
+ const sessionId = conversation?.threadSessionId ?? null;
406
+ const telaAgentId = conversation?.telaAgentId ?? null;
407
+ const createdAt = conversation?.createdAt ? new Date(conversation.createdAt) : null;
408
+ const datadogTimeRange = createdAt ? `&from_ts=${createdAt.getTime()}&to_ts=${Date.now()}&live=false` : "";
409
+ const datadogLogsUrl = (facet, value) => `https://us5.datadoghq.com/logs?query=${encodeURIComponent(`@${facet}:${value}`)}${datadogTimeRange}`;
410
+ const environment = useRuntimeConfig().public.environment;
411
+ const telaAppUrl = environment === "production" ? "https://app.tela.com" : "https://app.telastaging.com";
412
+ const debugInfo = {
413
+ conversationId: conversationId ?? null,
414
+ sessionId,
415
+ telaAgentId,
416
+ createdAt: createdAt?.toISOString() ?? null,
417
+ seeConversationInDatadog: conversationId ? datadogLogsUrl("conversationId", conversationId) : null,
418
+ seeSessionInDatadog: sessionId ? datadogLogsUrl("sessionId", sessionId) : null,
419
+ linkToTelaAgent: telaAgentId ? `${telaAppUrl}/agent/${telaAgentId}` : null
420
+ };
421
+ try {
422
+ await navigator.clipboard.writeText(JSON.stringify(debugInfo, null, 2));
423
+ statusToast.update({ text: "Dados de debug copiados", icon: "i-ph-check" });
424
+ } catch {
425
+ statusToast.update({ text: "Erro ao copiar dados de debug", icon: "i-ph-warning" });
426
+ }
427
+ }
401
428
  function startEditing() {
402
429
  editTitle.value = displayTitle.value;
403
430
  isEditing.value = true;
@@ -888,7 +915,7 @@ async function handleDelete() {
888
915
  <div flex="~ col" w-full my-24px gap-24px>
889
916
  <div flex="~ col" gap-4px w-full>
890
917
  <p text-14px font-580 text-gray-700 leading-18px>
891
- Chat Id
918
+ Conversation Id
892
919
  </p>
893
920
  <div
894
921
  px-12px py-10px
@@ -906,7 +933,7 @@ async function handleDelete() {
906
933
  </div>
907
934
  <div flex="~ col" gap-4px w-full>
908
935
  <p text-14px font-580 text-gray-700 leading-18px>
909
- Thread Session Id
936
+ Session Id
910
937
  </p>
911
938
  <div
912
939
  px-12px py-10px
@@ -923,6 +950,16 @@ async function handleDelete() {
923
950
  </div>
924
951
  </div>
925
952
  </div>
953
+ <template #footer>
954
+ <div flex justify-between items-center gap-12px w-full>
955
+ <p text-12px font-400 leading-16px text-tertiary>
956
+ Inclui os ids da conversa e os links de debug no Datadog e no Tela
957
+ </p>
958
+ <TelaButton variant="primary" icon="i-ph-copy" leading flex-shrink-0 @click="copyDebugInfo">
959
+ Copiar dados completos
960
+ </TelaButton>
961
+ </div>
962
+ </template>
926
963
  </TelaModal>
927
964
  </div>
928
965
 
@@ -939,6 +976,20 @@ async function handleDelete() {
939
976
  >
940
977
  <FilePreviewPanel />
941
978
  </div>
979
+
980
+ <div
981
+ v-if="isCitationPanelOpen && !isMobile"
982
+ h-full flex-shrink-0 pt-64px
983
+ w-440px
984
+ >
985
+ <CitationDocumentPanel />
986
+ </div>
987
+ <div
988
+ v-else-if="isCitationPanelOpen && isMobile"
989
+ fixed inset-0 z-50 bg-neutral-50
990
+ >
991
+ <CitationDocumentPanel />
992
+ </div>
942
993
  </div>
943
994
  </template>
944
995
 
@@ -20,6 +20,7 @@ const props = defineProps({
20
20
  loadingMessagesMode: { type: [String, null], required: false },
21
21
  customComponents: { type: Object, required: false },
22
22
  customHtmlTags: { type: Array, required: false },
23
+ citations: { type: [Object, null], required: false },
23
24
  feedbackConfig: { type: [Object, null], required: false },
24
25
  messageFeedback: { type: [Object, null], required: false },
25
26
  telaAgentId: { type: null, required: false },
@@ -49,6 +50,7 @@ provideEmbedConfig({
49
50
  loadingMessagesMode: computed(() => props.loadingMessagesMode),
50
51
  customComponents: computed(() => props.customComponents),
51
52
  customHtmlTags: computed(() => props.customHtmlTags),
53
+ citations: computed(() => props.citations),
52
54
  feedbackConfig: computed(() => props.feedbackConfig),
53
55
  messageFeedback: computed(() => props.messageFeedback)
54
56
  }, { syncGlobal: !parentEmbedConfig });
@@ -13,6 +13,7 @@ const props = defineProps({
13
13
  loadingMessagesMode: { type: [String, null], required: false },
14
14
  customComponents: { type: Object, required: false },
15
15
  customHtmlTags: { type: Array, required: false },
16
+ citations: { type: [Object, null], required: false },
16
17
  feedbackConfig: { type: [Object, null], required: false },
17
18
  messageFeedback: { type: [Object, null], required: false },
18
19
  telaAgentId: { type: null, required: false },
@@ -44,6 +45,7 @@ const hideSettings = computed(() => props.hideSettings ?? false);
44
45
  :loading-messages-mode="loadingMessagesMode"
45
46
  :custom-components="customComponents"
46
47
  :custom-html-tags="customHtmlTags"
48
+ :citations="citations"
47
49
  :feedback-config="feedbackConfig"
48
50
  :message-feedback="messageFeedback"
49
51
  @update:conversation-id="$emit('update:conversationId', $event)"
@@ -72,6 +74,7 @@ const hideSettings = computed(() => props.hideSettings ?? false);
72
74
  :loading-messages-mode="loadingMessagesMode"
73
75
  :custom-components="customComponents"
74
76
  :custom-html-tags="customHtmlTags"
77
+ :citations="citations"
75
78
  :feedback-config="feedbackConfig"
76
79
  :message-feedback="messageFeedback"
77
80
  @update:conversation-id="$emit('update:conversationId', $event)"
@@ -310,7 +310,7 @@ async function handleConversationDrop(event, groupId) {
310
310
  </div>
311
311
  <div v-else flex="~ col" gap-8px>
312
312
  <div
313
- v-if="showGroupCreationPrompt"
313
+ v-if="showGroupCreationPrompt && !hideFolderHint"
314
314
  mx-4px
315
315
  flex="~ col" gap-10px
316
316
  >
@@ -339,7 +339,7 @@ async function handleConversationDrop(event, groupId) {
339
339
  <span h-10px w-84px rounded-full bg-neutral-100 />
340
340
  </div>
341
341
  </div>
342
- <p v-if="!hideFolderHint" px-4px text-14px text-neutral-500 leading-20px>
342
+ <p px-4px text-14px text-neutral-500 leading-20px>
343
343
  Suas pastas aparecem aqui. Crie uma para começar a agrupar conversas.
344
344
  </p>
345
345
  <ChatConversationNewGroupButton label="Criar pasta" variant="empty-state" />
@@ -3,6 +3,7 @@ import type { ConversationCreatorFilter } from '../../types/schemas/chat/primiti
3
3
  import type { WorkspaceSettingsResponse as WorkspaceSettings } from '../../types/schemas/chat/workspace-settings.js';
4
4
  import type { TelaAgentExecutionInput } from '../../types/schemas/integrations/tela-agent-api.js';
5
5
  import type { ChatLoadingMessageMode } from '../loading-messages.js';
6
+ import type { ChatCitationsConfig } from '../../citations/types.js';
6
7
  import type { MessageFeedbackConfig, MessageFeedbackRating } from '../../messages/composables/chat-feedback.js';
7
8
  type ConfiguredTelaAgentInputs = DeepReadonly<TelaAgentExecutionInput[]> | null | undefined;
8
9
  type ConfiguredMessageFeedback = Readonly<Record<string, MessageFeedbackRating>> | null | undefined;
@@ -19,6 +20,7 @@ export type EmbedConfig = {
19
20
  loadingMessagesMode: Ref<ChatLoadingMessageMode | null | undefined>;
20
21
  customComponents: Ref<Record<string, Component> | undefined>;
21
22
  customHtmlTags: Ref<readonly string[] | undefined>;
23
+ citations: Ref<ChatCitationsConfig | null | undefined>;
22
24
  feedbackConfig: Ref<MessageFeedbackConfig | null | undefined>;
23
25
  messageFeedback: Ref<ConfiguredMessageFeedback>;
24
26
  };
@@ -35,6 +37,7 @@ type EmbedConfigInput = {
35
37
  loadingMessagesMode?: MaybeRefOrGetter<ChatLoadingMessageMode | null | undefined>;
36
38
  customComponents?: MaybeRefOrGetter<Record<string, Component> | undefined>;
37
39
  customHtmlTags?: MaybeRefOrGetter<readonly string[] | undefined>;
40
+ citations?: MaybeRefOrGetter<ChatCitationsConfig | null | undefined>;
38
41
  feedbackConfig?: MaybeRefOrGetter<MessageFeedbackConfig | null | undefined>;
39
42
  messageFeedback?: MaybeRefOrGetter<ConfiguredMessageFeedback>;
40
43
  };
@@ -5,6 +5,11 @@ function useGlobalCustomComponentsState() {
5
5
  nuxtApp._chatEmbedConfigCustomComponents ??= shallowRef(void 0);
6
6
  return nuxtApp._chatEmbedConfigCustomComponents;
7
7
  }
8
+ function useGlobalCitationsState() {
9
+ const nuxtApp = useNuxtApp();
10
+ nuxtApp._chatEmbedConfigCitations ??= shallowRef(void 0);
11
+ return nuxtApp._chatEmbedConfigCitations;
12
+ }
8
13
  function useGlobalEmbedConfigState() {
9
14
  return {
10
15
  provided: useState("chat-embed-config-provided", () => false),
@@ -20,6 +25,7 @@ function useGlobalEmbedConfigState() {
20
25
  loadingMessagesMode: useState("chat-embed-config-loading-messages-mode", () => void 0),
21
26
  customComponents: useGlobalCustomComponentsState(),
22
27
  customHtmlTags: useState("chat-embed-config-custom-html-tags", () => void 0),
28
+ citations: useGlobalCitationsState(),
23
29
  feedbackConfig: useState("chat-embed-config-feedback-config", () => void 0),
24
30
  messageFeedback: useState("chat-embed-config-message-feedback", () => void 0)
25
31
  };
@@ -38,6 +44,7 @@ export function provideEmbedConfig(input, options = {}) {
38
44
  loadingMessagesMode: computed(() => toValue(input.loadingMessagesMode)),
39
45
  customComponents: computed(() => toValue(input.customComponents)),
40
46
  customHtmlTags: computed(() => toValue(input.customHtmlTags)),
47
+ citations: computed(() => toValue(input.citations)),
41
48
  feedbackConfig: computed(() => toValue(input.feedbackConfig)),
42
49
  messageFeedback: computed(() => toValue(input.messageFeedback))
43
50
  };
@@ -57,6 +64,7 @@ export function provideEmbedConfig(input, options = {}) {
57
64
  globalConfig.loadingMessagesMode.value = config.loadingMessagesMode.value;
58
65
  globalConfig.customComponents.value = config.customComponents.value;
59
66
  globalConfig.customHtmlTags.value = config.customHtmlTags.value;
67
+ globalConfig.citations.value = config.citations.value;
60
68
  globalConfig.feedbackConfig.value = config.feedbackConfig.value;
61
69
  globalConfig.messageFeedback.value = config.messageFeedback.value;
62
70
  });
@@ -83,6 +91,7 @@ export function useEmbedConfig() {
83
91
  loadingMessagesMode: computed(() => globalConfig.loadingMessagesMode.value),
84
92
  customComponents: computed(() => globalConfig.customComponents.value),
85
93
  customHtmlTags: computed(() => globalConfig.customHtmlTags.value),
94
+ citations: computed(() => globalConfig.citations.value),
86
95
  feedbackConfig: computed(() => globalConfig.feedbackConfig.value),
87
96
  messageFeedback: computed(() => globalConfig.messageFeedback.value)
88
97
  };
@@ -4,6 +4,7 @@ import type { TelaAgentExecutionInput } from '../types/schemas/integrations/tela
4
4
  import type { ConversationCreatorFilter } from '../types/schemas/chat/primitives.js';
5
5
  import type { ChatLoadingMessageMode } from './loading-messages.js';
6
6
  import type { ChatFeatureConfig } from './feature-config.js';
7
+ import type { ChatCitationsConfig } from '../citations/types.js';
7
8
  import type { MessageFeedbackConfig, MessageFeedbackRating } from '../messages/composables/chat-feedback.js';
8
9
  export type ChatActor = {
9
10
  name?: string | null;
@@ -44,7 +45,11 @@ export type ChatEmbedSharedProps = {
44
45
  sidebar?: ChatSidebarConfig;
45
46
  /** Hide workspace settings actions while keeping the chat header and content unchanged. */
46
47
  hideSettings?: boolean;
47
- /** Hide the folder onboarding hint while keeping folder creation actions available. */
48
+ /**
49
+ * Hide the empty-state folder onboarding block (PASTAS header, ghost rows, hint
50
+ * text and inline create button). Folder creation stays available through the
51
+ * new-group button in the sidebar header.
52
+ */
48
53
  hideFolderHint?: boolean;
49
54
  /** Custom pending-response messages shown while the assistant is working. */
50
55
  loadingMessages?: readonly string[] | null;
@@ -54,6 +59,12 @@ export type ChatEmbedSharedProps = {
54
59
  customComponents?: Record<string, Component>;
55
60
  /** Extra HTML tags allowed by the markdown renderer. */
56
61
  customHtmlTags?: readonly string[];
62
+ /**
63
+ * Enables `cite://` citations in assistant messages. The host resolves each
64
+ * citation against its own backend (carrying the viewing user's authorization);
65
+ * the lib renders chips, the metadata popover and the document panel.
66
+ */
67
+ citations?: ChatCitationsConfig | null;
57
68
  /**
58
69
  * Enables 👍/👎 feedback on assistant messages and configures the reason
59
70
  * popovers. Providing the prop (even `{}`) shows the controls; votes reach
@@ -1,11 +1,19 @@
1
1
  <script setup>
2
+ import { parseCitationUrl } from "../../citations/citation-links";
3
+ import { useEmbedConfig } from "../../embed/composables/embed-config";
2
4
  import { detectFileFromUrl } from "../content-detection";
3
5
  const props = defineProps({
4
6
  node: { type: Object, required: true },
5
7
  indexKey: { type: [Number, String], required: true },
6
8
  customId: { type: String, required: false }
7
9
  });
10
+ const embedConfig = useEmbedConfig();
8
11
  const detectedFile = computed(() => detectFileFromUrl(props.node.href));
12
+ const detectedCitation = computed(() => {
13
+ if (!embedConfig?.citations.value)
14
+ return null;
15
+ return parseCitationUrl(props.node.href);
16
+ });
9
17
  const linkText = computed(() => {
10
18
  if (props.node.children?.length > 0) {
11
19
  return props.node.children.map((c) => c.raw).join("");
@@ -23,6 +31,11 @@ const linkText = computed(() => {
23
31
  w-full
24
32
  my-4px
25
33
  />
34
+ <ChatCitation
35
+ v-else-if="detectedCitation"
36
+ :citation="detectedCitation"
37
+ :label="linkText"
38
+ />
26
39
  <a
27
40
  v-else
28
41
  :href="node.href"
@@ -1,3 +1,4 @@
1
+ import { type CitationRef } from '../citations/citation-links.js';
1
2
  import { type VaultHashParams } from './vault-links.js';
2
3
  export type DetectedFile = {
3
4
  type: 'vault' | 'url';
@@ -12,7 +13,14 @@ export type ContentSegment = {
12
13
  } | {
13
14
  type: 'file';
14
15
  file: DetectedFile;
16
+ } | {
17
+ type: 'citation';
18
+ citation: CitationRef;
19
+ };
20
+ export type ParseContentOptions = {
21
+ /** Detect bare `cite://` tokens as citation segments. Off by default: only hosts that configured citations opt in. */
22
+ detectCitations?: boolean;
15
23
  };
16
24
  export declare function detectFilesInText(text: string): DetectedFile[];
17
- export declare function parseContentWithFiles(text: string): ContentSegment[];
25
+ export declare function parseContentWithFiles(text: string, options?: ParseContentOptions): ContentSegment[];
18
26
  export declare function detectFileFromUrl(url: string): DetectedFile | null;
@@ -1,3 +1,4 @@
1
+ import { CITE_REGEX, parseCitationUrl } from "../citations/citation-links.js";
1
2
  import { getExtensionFromUrl, getMimeTypeFromExtension, isFileUrl } from "./mime.js";
2
3
  import {
3
4
  getPermalinkId,
@@ -128,7 +129,7 @@ export function detectFilesInText(text) {
128
129
  }
129
130
  return files;
130
131
  }
131
- export function parseContentWithFiles(text) {
132
+ export function parseContentWithFiles(text, options) {
132
133
  const segments = [];
133
134
  const fileMatches = collectMarkdownFileLinkMatches(text);
134
135
  for (const match of text.matchAll(VAULT_REGEX)) {
@@ -198,18 +199,38 @@ export function parseContentWithFiles(text) {
198
199
  });
199
200
  }
200
201
  }
201
- if (fileMatches.length === 0)
202
+ const matches = fileMatches.map((fm) => ({
203
+ start: fm.start,
204
+ end: fm.end,
205
+ segment: { type: "file", file: fm.file }
206
+ }));
207
+ if (options?.detectCitations) {
208
+ for (const match of text.matchAll(CITE_REGEX)) {
209
+ const url = stripTrailingPunctuation(match[0]);
210
+ if (overlapsFileMatch(match.index, match.index + url.length, fileMatches))
211
+ continue;
212
+ const citation = parseCitationUrl(url);
213
+ if (!citation)
214
+ continue;
215
+ matches.push({
216
+ start: match.index,
217
+ end: match.index + url.length,
218
+ segment: { type: "citation", citation }
219
+ });
220
+ }
221
+ }
222
+ if (matches.length === 0)
202
223
  return [{ type: "text", content: text }];
203
- fileMatches.sort((a, b) => a.start - b.start);
224
+ matches.sort((a, b) => a.start - b.start);
204
225
  let lastEnd = 0;
205
- for (const fm of fileMatches) {
206
- if (fm.start > lastEnd) {
207
- const textContent = text.slice(lastEnd, fm.start);
226
+ for (const m of matches) {
227
+ if (m.start > lastEnd) {
228
+ const textContent = text.slice(lastEnd, m.start);
208
229
  if (textContent)
209
230
  segments.push({ type: "text", content: textContent });
210
231
  }
211
- segments.push({ type: "file", file: fm.file });
212
- lastEnd = fm.end;
232
+ segments.push(m.segment);
233
+ lastEnd = m.end;
213
234
  }
214
235
  if (lastEnd < text.length) {
215
236
  const textContent = text.slice(lastEnd);
@@ -98,9 +98,9 @@ const contentSegments = computed(() => {
98
98
  const content = displayContent.value;
99
99
  if (!content)
100
100
  return [];
101
- return parseContentWithFiles(content);
101
+ return parseContentWithFiles(content, { detectCitations: Boolean(embedConfig?.citations.value) });
102
102
  });
103
- const hasDetectedFiles = computed(() => contentSegments.value.some((s) => s.type === "file"));
103
+ const hasDetectedFiles = computed(() => contentSegments.value.some((s) => s.type !== "text"));
104
104
  </script>
105
105
 
106
106
  <template>
@@ -258,13 +258,17 @@ const hasDetectedFiles = computed(() => contentSegments.value.some((s) => s.type
258
258
  />
259
259
  </ClientOnly>
260
260
  <ChatFilePreview
261
- v-else
261
+ v-else-if="segment.type === 'file'"
262
262
  :file="segment.file"
263
263
  inline-block
264
264
  align-middle
265
265
  w-full
266
266
  my-4px
267
267
  />
268
+ <ChatCitation
269
+ v-else
270
+ :citation="segment.citation"
271
+ />
268
272
  </template>
269
273
  </div>
270
274
  <ClientOnly v-else>
@@ -58,8 +58,17 @@ function buildProxyHeaders(event) {
58
58
  });
59
59
  }
60
60
  const headers = {
61
- authorization: `Bearer ${token}`
61
+ "authorization": `Bearer ${token}`,
62
+ "x-chat-client": "chat-nuxt"
62
63
  };
64
+ const config = useRuntimeConfig(event);
65
+ if (config.chatNuxt?.clientVersion) {
66
+ headers["x-chat-client-version"] = config.chatNuxt.clientVersion;
67
+ }
68
+ const source = config.chatNuxt?.source || getRequestURL(event).host;
69
+ if (source) {
70
+ headers["x-chat-source"] = source;
71
+ }
63
72
  for (const headerName of CHAT_COMPAT_HEADERS) {
64
73
  const value = getHeader(event, headerName)?.trim();
65
74
  if (value) {
@@ -8,7 +8,14 @@ type Workspace = {
8
8
  id: string;
9
9
  name?: string | null;
10
10
  };
11
- export declare function initPosthog(publicKey: string, host: string, defaults: string): Promise<void>;
11
+ export type PosthogLogsOptions = {
12
+ captureConsoleLogs?: boolean;
13
+ /** Shown as `service.name` in the PostHog Logs UI. Defaults to `unknown_service`. */
14
+ serviceName?: string;
15
+ /** Shown as `deployment.environment` in the PostHog Logs UI. */
16
+ environment?: string;
17
+ };
18
+ export declare function initPosthog(publicKey: string, host: string, defaults: string, logs?: PosthogLogsOptions): Promise<void>;
12
19
  export declare function identifyPosthogUser(user: AuthUser): void;
13
20
  export declare function resetPosthogIdentity(): void;
14
21
  export declare function setPosthogWorkspace(workspace: Workspace): void;
@@ -1,7 +1,7 @@
1
1
  const posthogClient = shallowRef(null);
2
2
  const flagsVersion = ref(0);
3
3
  let identifiedUserFingerprint = null;
4
- export async function initPosthog(publicKey, host, defaults) {
4
+ export async function initPosthog(publicKey, host, defaults, logs) {
5
5
  if (posthogClient.value)
6
6
  return;
7
7
  try {
@@ -10,6 +10,15 @@ export async function initPosthog(publicKey, host, defaults) {
10
10
  api_host: host,
11
11
  defaults,
12
12
  person_profiles: "identified_only",
13
+ // serviceName/environment are supported at runtime but missing
14
+ // from PostHogConfig['logs'] in the pinned posthog-js version.
15
+ ...logs?.captureConsoleLogs ? {
16
+ logs: {
17
+ captureConsoleLogs: true,
18
+ serviceName: logs.serviceName,
19
+ environment: logs.environment
20
+ }
21
+ } : {},
13
22
  loaded: (c) => {
14
23
  if (import.meta.dev)
15
24
  c.debug();