@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.
- package/README.md +26 -0
- package/dist/module.d.mts +2 -0
- package/dist/module.json +1 -1
- package/dist/module.mjs +6 -3
- package/dist/runtime/citations/citation-links.d.ts +32 -0
- package/dist/runtime/citations/citation-links.js +56 -0
- package/dist/runtime/citations/components/chat-citation.d.vue.ts +9 -0
- package/dist/runtime/citations/components/chat-citation.vue +186 -0
- package/dist/runtime/citations/components/chat-citation.vue.d.ts +9 -0
- package/dist/runtime/citations/components/citation-document-panel.d.vue.ts +3 -0
- package/dist/runtime/citations/components/citation-document-panel.vue +217 -0
- package/dist/runtime/citations/components/citation-document-panel.vue.d.ts +3 -0
- package/dist/runtime/citations/composables/citation-panel.d.ts +8 -0
- package/dist/runtime/citations/composables/citation-panel.js +17 -0
- package/dist/runtime/citations/composables/citations.d.ts +17 -0
- package/dist/runtime/citations/composables/citations.js +44 -0
- package/dist/runtime/citations/document-highlight.d.ts +13 -0
- package/dist/runtime/citations/document-highlight.js +35 -0
- package/dist/runtime/citations/types.d.ts +57 -0
- package/dist/runtime/citations/types.js +0 -0
- package/dist/runtime/conversations/components/chat-conversation-list.vue +2 -2
- package/dist/runtime/conversations/composables/conversations.js +5 -0
- package/dist/runtime/embed/components/chat-embed-inner.vue +53 -2
- package/dist/runtime/embed/components/chat-embed.vue +2 -0
- package/dist/runtime/embed/components/meistrari-chat-embed.vue +3 -0
- package/dist/runtime/embed/components/mobile/chat-mobile-drawer.vue +2 -2
- package/dist/runtime/embed/composables/embed-config.d.ts +3 -0
- package/dist/runtime/embed/composables/embed-config.js +9 -0
- package/dist/runtime/embed/types.d.ts +12 -1
- package/dist/runtime/files/components/chat-vault-link.vue +13 -0
- package/dist/runtime/files/content-detection.d.ts +9 -1
- package/dist/runtime/files/content-detection.js +29 -8
- package/dist/runtime/messages/components/chat-message-bubble.vue +7 -3
- package/dist/runtime/server/utils/chat-api-proxy.js +10 -1
- package/dist/runtime/shared/composables/feature-flags.d.ts +8 -1
- package/dist/runtime/shared/composables/feature-flags.js +10 -1
- package/dist/runtime/types/citations.d.ts +1 -0
- package/dist/runtime/types/citations.js +0 -0
- package/dist/runtime/types/embed.d.ts +1 -0
- package/dist/runtime/types/schemas/chat/usage.d.ts +2 -0
- package/dist/runtime/types/schemas/chat/usage.js +1 -0
- 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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
/**
|
|
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
|
-
|
|
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
|
-
|
|
224
|
+
matches.sort((a, b) => a.start - b.start);
|
|
204
225
|
let lastEnd = 0;
|
|
205
|
-
for (const
|
|
206
|
-
if (
|
|
207
|
-
const textContent = text.slice(lastEnd,
|
|
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(
|
|
212
|
-
lastEnd =
|
|
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
|
|
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
|
|
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();
|