@meistrari/chat-nuxt 3.7.0 → 3.9.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.json +1 -1
- package/dist/module.mjs +4 -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 +241 -0
- package/dist/runtime/citations/components/citation-document-panel.vue.d.ts +3 -0
- package/dist/runtime/citations/components/citation-highlight.d.vue.ts +19 -0
- package/dist/runtime/citations/components/citation-highlight.vue +16 -0
- package/dist/runtime/citations/components/citation-highlight.vue.d.ts +19 -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 +14 -0
- package/dist/runtime/citations/document-highlight.js +43 -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/embed/components/chat-embed-inner.vue +15 -0
- package/dist/runtime/embed/components/chat-embed.vue +2 -0
- package/dist/runtime/embed/components/configuration/chat-configuration-knowledge-sources-tab.d.vue.ts +2 -2
- package/dist/runtime/embed/components/configuration/chat-configuration-knowledge-sources-tab.vue.d.ts +2 -2
- 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/components/mobile/chat-mobile-shell.d.vue.ts +2 -2
- package/dist/runtime/embed/components/mobile/chat-mobile-shell.vue.d.ts +2 -2
- package/dist/runtime/embed/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/messages/components/reasoning/chat-reasoning-message.vue +22 -266
- package/dist/runtime/messages/components/reasoning/chat-reasoning-thought.vue +2 -3
- package/dist/runtime/messages/tool-registry.d.ts +15 -0
- package/dist/runtime/messages/tool-registry.js +138 -0
- 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/package.json +1 -1
- package/dist/runtime/messages/composables/tool-formatting.d.ts +0 -5
- package/dist/runtime/messages/composables/tool-formatting.js +0 -61
- package/dist/runtime/shared/composables/verb-conjugation.d.ts +0 -7
- package/dist/runtime/shared/composables/verb-conjugation.js +0 -269
package/README.md
CHANGED
|
@@ -211,6 +211,7 @@ import type {
|
|
|
211
211
|
| `loadingMessagesMode` | `'ordered' \| 'random' \| null` | `'ordered'` | Show custom messages sequentially or randomly |
|
|
212
212
|
| `customComponents` | `Record<string, Component>` | `undefined` | Custom markdown renderers keyed by markdown node or tag name |
|
|
213
213
|
| `customHtmlTags` | `readonly string[]` | `undefined` | Extra HTML tags allowed by the markdown renderer |
|
|
214
|
+
| `citations` | `ChatCitationsConfig \| null` | `undefined` | Enables `cite://` citations in assistant messages (see Citations below) |
|
|
214
215
|
| `feedbackConfig` | `MessageFeedbackConfig \| null` | `undefined` | Enables 👍/👎 feedback on assistant messages; providing the prop (even `{}`) shows the controls |
|
|
215
216
|
| `messageFeedback` | `Record<string, MessageFeedbackRating> \| null` | `undefined` | Host-persisted votes keyed by message id; when provided, it is the single source of truth for the selected thumbs |
|
|
216
217
|
|
|
@@ -232,6 +233,31 @@ Scoped embeds only see conversations created with the same scope. Unscoped embed
|
|
|
232
233
|
/>
|
|
233
234
|
```
|
|
234
235
|
|
|
236
|
+
**Citations (`cite://`):**
|
|
237
|
+
|
|
238
|
+
Hosts whose agents cite retrievable documents can teach them (via prompt) the notation
|
|
239
|
+
|
|
240
|
+
```
|
|
241
|
+
[visible text](cite://<namespace>/<id>[#q=<term>&n=<nth>&p=<page>])
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
and pass `citations` to enable it. The lib detects the notation in assistant messages (markdown links and bare tokens), renders each citation as a chip with a metadata popover, and — when `getDocument` is provided — opens a built-in side panel that shows the document with the cited term highlighted (`#q`, nth occurrence via `#n`). What a namespace means, and every data access, belongs to the host: callbacks run against the host's own backend, carrying the viewing user's authorization. A citation that fails to resolve renders as "unverified" — an agent cannot fabricate a trusted-looking reference, and a viewer without access to a document learns nothing from a citation to it.
|
|
245
|
+
|
|
246
|
+
```vue
|
|
247
|
+
<MeistrariChatEmbed
|
|
248
|
+
tela-agent-id="agent_123"
|
|
249
|
+
:citations="{
|
|
250
|
+
resolve: ref => $fetch(`/api/citations/${ref.namespace}/${encodeURIComponent(ref.id)}`).catch(() => null),
|
|
251
|
+
getDocument: ref => $fetch(`/api/citations/${ref.namespace}/${encodeURIComponent(ref.id)}/document`).catch(() => null),
|
|
252
|
+
locate: (ref, term) => $fetch(`/api/citations/${ref.namespace}/${encodeURIComponent(ref.id)}/occurrences`, { query: { term } }).catch(() => null),
|
|
253
|
+
}"
|
|
254
|
+
/>
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
`ref.id` arrives percent-decoded (natural identifiers may carry pipes, spaces, accents or slashes) — re-encode it when building URLs, as above.
|
|
258
|
+
|
|
259
|
+
`ChatCitationsConfig` (exported from `@meistrari/chat-nuxt/types/citations`): `resolve` maps a citation to display metadata (`null` = unverified); `getDocument` enables the document panel; `locate` returns precise term offsets for highlighting (omit it for a client-side, accent-sensitive fallback); `component` replaces the built-in chip; `onOpen` takes over the click entirely. Without the `citations` prop nothing changes: `cite://` hrefs keep rendering as plain links.
|
|
260
|
+
|
|
235
261
|
**Events:**
|
|
236
262
|
|
|
237
263
|
| Event | Payload | Description |
|
package/dist/module.json
CHANGED
package/dist/module.mjs
CHANGED
|
@@ -2,6 +2,7 @@ import { existsSync, readFileSync, mkdirSync, symlinkSync } from 'node:fs';
|
|
|
2
2
|
import { createRequire } from 'node:module';
|
|
3
3
|
import { join, dirname } from 'node:path';
|
|
4
4
|
import { defineNuxtModule, createResolver, addTemplate, addTypeTemplate, addPlugin, addImportsDir, addComponent, addComponentsDir } from 'nuxt/kit';
|
|
5
|
+
import { TOOL_ICON_SAFELIST } from '../dist/runtime/messages/tool-registry.js';
|
|
5
6
|
export { ChatMessageStatus } from '../dist/runtime/messages/composables/chat-message-terminal.js';
|
|
6
7
|
|
|
7
8
|
const localRequire = createRequire(import.meta.url);
|
|
@@ -151,8 +152,8 @@ function resolveWorkspaceSettingsResolverEntry(srcDir, fallbackResolverBase) {
|
|
|
151
152
|
const _chatNuxtRequire = createRequire(import.meta.url);
|
|
152
153
|
const _chatNuxtPackageVersion = _chatNuxtRequire("../package.json").version;
|
|
153
154
|
const VUE_COMPONENT_EXTENSIONS = [".vue"];
|
|
154
|
-
const FEATURE_DIRS = ["conversations", "embed", "files", "messages", "rendering", "shared", "usage", "workspace"];
|
|
155
|
-
const FULL_NAME_COMPONENT_DIRS = ["conversations", "embed", "files", "messages", "usage"];
|
|
155
|
+
const FEATURE_DIRS = ["citations", "conversations", "embed", "files", "messages", "rendering", "shared", "usage", "workspace"];
|
|
156
|
+
const FULL_NAME_COMPONENT_DIRS = ["citations", "conversations", "embed", "files", "messages", "usage"];
|
|
156
157
|
const PATH_PREFIX_COMPONENT_DIRS = ["rendering", "shared"];
|
|
157
158
|
const DYNAMIC_ICON_SAFELIST = [
|
|
158
159
|
"i-ph-folder-minus",
|
|
@@ -257,7 +258,7 @@ function ensureDynamicIconSafelist(nuxtOptions) {
|
|
|
257
258
|
const options = nuxtOptions;
|
|
258
259
|
const unocssOptions = options.unocss ||= {};
|
|
259
260
|
const currentSafelist = Array.isArray(unocssOptions.safelist) ? unocssOptions.safelist : unocssOptions.safelist ? [unocssOptions.safelist] : [];
|
|
260
|
-
for (const icon of DYNAMIC_ICON_SAFELIST) {
|
|
261
|
+
for (const icon of [...DYNAMIC_ICON_SAFELIST, ...TOOL_ICON_SAFELIST]) {
|
|
261
262
|
if (!currentSafelist.includes(icon)) {
|
|
262
263
|
currentSafelist.push(icon);
|
|
263
264
|
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `cite://` notation: `[visible text](cite://<namespace>/<id>[#q=<term>&n=<nth>&p=<page>])`.
|
|
3
|
+
*
|
|
4
|
+
* Agents are taught (via prompt) to emit this scheme when citing a document they actually
|
|
5
|
+
* retrieved. The lib only parses the shape — what a namespace means and how a citation is
|
|
6
|
+
* resolved belongs to the host app through `ChatCitationsConfig`. The id is expected to be
|
|
7
|
+
* an identifier the agent could only know from a real retrieval, so a citation that fails
|
|
8
|
+
* to resolve is rendered as unverified instead of trusted.
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* Bare cite tokens in prose; markdown link targets (`](cite://...`) are left to the link
|
|
12
|
+
* renderer. Unlike VAULT_REGEX, `_` is allowed: source namespaces use it (`stf_sumulas`).
|
|
13
|
+
*/
|
|
14
|
+
export declare const CITE_REGEX: RegExp;
|
|
15
|
+
export type CitationLocator = {
|
|
16
|
+
/** Exact term whose occurrence the citation points at, for in-document highlight. */
|
|
17
|
+
term?: string;
|
|
18
|
+
/** 1-based nth occurrence of `term`; defaults to the first when absent. */
|
|
19
|
+
occurrence?: number;
|
|
20
|
+
/** 1-based page at the original source; only meaningful for paginated originals. */
|
|
21
|
+
page?: number;
|
|
22
|
+
};
|
|
23
|
+
export type CitationRef = {
|
|
24
|
+
namespace: string;
|
|
25
|
+
id: string;
|
|
26
|
+
locator?: CitationLocator;
|
|
27
|
+
raw: string;
|
|
28
|
+
};
|
|
29
|
+
export declare function isCitationUrl(url: string): boolean;
|
|
30
|
+
export declare function parseCitationUrl(url: string): CitationRef | null;
|
|
31
|
+
/** Cache key for resolution state: the locator addresses a spot inside the same document. */
|
|
32
|
+
export declare function citationCacheKey(ref: Pick<CitationRef, 'namespace' | 'id'>): string;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
const CITE_SCHEME = "cite://";
|
|
2
|
+
export const CITE_REGEX = /(?<!\]\()cite:\/\/[^\s"'<>`\[\]()*~]+/gi;
|
|
3
|
+
const NAMESPACE_PATTERN = /^[a-z0-9_-]+$/i;
|
|
4
|
+
export function isCitationUrl(url) {
|
|
5
|
+
return url.toLowerCase().startsWith(CITE_SCHEME);
|
|
6
|
+
}
|
|
7
|
+
export function parseCitationUrl(url) {
|
|
8
|
+
if (!isCitationUrl(url))
|
|
9
|
+
return null;
|
|
10
|
+
const hashIndex = url.indexOf("#");
|
|
11
|
+
const body = hashIndex === -1 ? url.slice(CITE_SCHEME.length) : url.slice(CITE_SCHEME.length, hashIndex);
|
|
12
|
+
const separatorIndex = body.indexOf("/");
|
|
13
|
+
if (separatorIndex === -1)
|
|
14
|
+
return null;
|
|
15
|
+
const namespace = body.slice(0, separatorIndex);
|
|
16
|
+
const id = safeDecodeURIComponent(body.slice(separatorIndex + 1));
|
|
17
|
+
if (!namespace || !NAMESPACE_PATTERN.test(namespace) || !id)
|
|
18
|
+
return null;
|
|
19
|
+
const locator = hashIndex === -1 ? void 0 : parseCitationLocator(url.slice(hashIndex + 1));
|
|
20
|
+
return {
|
|
21
|
+
namespace: namespace.toLowerCase(),
|
|
22
|
+
id,
|
|
23
|
+
...locator ? { locator } : {},
|
|
24
|
+
raw: url
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
function parseCitationLocator(hash) {
|
|
28
|
+
const params = new URLSearchParams(hash);
|
|
29
|
+
const locator = {};
|
|
30
|
+
const term = params.get("q")?.trim();
|
|
31
|
+
if (term)
|
|
32
|
+
locator.term = term;
|
|
33
|
+
const occurrence = parsePositiveInt(params.get("n"));
|
|
34
|
+
if (occurrence !== void 0)
|
|
35
|
+
locator.occurrence = occurrence;
|
|
36
|
+
const page = parsePositiveInt(params.get("p"));
|
|
37
|
+
if (page !== void 0)
|
|
38
|
+
locator.page = page;
|
|
39
|
+
return Object.keys(locator).length > 0 ? locator : void 0;
|
|
40
|
+
}
|
|
41
|
+
function safeDecodeURIComponent(value) {
|
|
42
|
+
try {
|
|
43
|
+
return decodeURIComponent(value);
|
|
44
|
+
} catch {
|
|
45
|
+
return value;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
function parsePositiveInt(value) {
|
|
49
|
+
if (!value || !/^\d+$/.test(value))
|
|
50
|
+
return void 0;
|
|
51
|
+
const parsed = Number(value);
|
|
52
|
+
return Number.isSafeInteger(parsed) && parsed > 0 ? parsed : void 0;
|
|
53
|
+
}
|
|
54
|
+
export function citationCacheKey(ref) {
|
|
55
|
+
return `${ref.namespace}/${ref.id}`;
|
|
56
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { CitationRef } from '../citation-links.js';
|
|
2
|
+
type __VLS_Props = {
|
|
3
|
+
citation: CitationRef;
|
|
4
|
+
/** Visible text when the citation came from a markdown link. */
|
|
5
|
+
label?: string;
|
|
6
|
+
};
|
|
7
|
+
declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
8
|
+
declare const _default: typeof __VLS_export;
|
|
9
|
+
export default _default;
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { onClickOutside } from "@vueuse/core";
|
|
3
|
+
import { useEmbedConfig } from "../../embed/composables/embed-config";
|
|
4
|
+
import { useCitationPanel } from "../composables/citation-panel";
|
|
5
|
+
import { useCitations } from "../composables/citations";
|
|
6
|
+
const props = defineProps({
|
|
7
|
+
citation: { type: Object, required: true },
|
|
8
|
+
label: { type: String, required: false }
|
|
9
|
+
});
|
|
10
|
+
const embedConfig = useEmbedConfig();
|
|
11
|
+
const config = computed(() => embedConfig?.citations.value);
|
|
12
|
+
const { resolveCitation, retryCitation } = useCitations();
|
|
13
|
+
const { openCitationDocument } = useCitationPanel();
|
|
14
|
+
const entry = shallowRef(null);
|
|
15
|
+
watchEffect(() => {
|
|
16
|
+
if (!entry.value && config.value)
|
|
17
|
+
entry.value = resolveCitation(config.value, props.citation);
|
|
18
|
+
});
|
|
19
|
+
const status = computed(() => entry.value?.status.value ?? "loading");
|
|
20
|
+
const meta = computed(() => entry.value?.meta.value ?? null);
|
|
21
|
+
const displayLabel = computed(() => props.label?.trim() || meta.value?.identifier || meta.value?.title || "Documento");
|
|
22
|
+
const rootEl = ref(null);
|
|
23
|
+
const popoverOpen = ref(false);
|
|
24
|
+
onClickOutside(rootEl, () => {
|
|
25
|
+
popoverOpen.value = false;
|
|
26
|
+
});
|
|
27
|
+
function togglePopover() {
|
|
28
|
+
if (status.value === "loading")
|
|
29
|
+
return;
|
|
30
|
+
popoverOpen.value = !popoverOpen.value;
|
|
31
|
+
}
|
|
32
|
+
function retry() {
|
|
33
|
+
if (!config.value)
|
|
34
|
+
return;
|
|
35
|
+
entry.value = retryCitation(config.value, props.citation);
|
|
36
|
+
popoverOpen.value = false;
|
|
37
|
+
}
|
|
38
|
+
const canOpenDocument = computed(() => Boolean(config.value?.onOpen || config.value?.getDocument));
|
|
39
|
+
function openDocument() {
|
|
40
|
+
popoverOpen.value = false;
|
|
41
|
+
const activeConfig = config.value;
|
|
42
|
+
if (!activeConfig)
|
|
43
|
+
return;
|
|
44
|
+
if (activeConfig.onOpen) {
|
|
45
|
+
activeConfig.onOpen(props.citation, meta.value);
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
openCitationDocument(props.citation, meta.value);
|
|
49
|
+
}
|
|
50
|
+
const safeSourceUrl = computed(() => {
|
|
51
|
+
const url = meta.value?.url;
|
|
52
|
+
return url && /^https?:\/\//i.test(url) ? url : null;
|
|
53
|
+
});
|
|
54
|
+
const formattedDate = computed(() => {
|
|
55
|
+
if (!meta.value?.date)
|
|
56
|
+
return null;
|
|
57
|
+
const parsed = new Date(meta.value.date);
|
|
58
|
+
if (Number.isNaN(parsed.getTime()))
|
|
59
|
+
return meta.value.date;
|
|
60
|
+
return parsed.toLocaleDateString("pt-BR", { timeZone: "UTC" });
|
|
61
|
+
});
|
|
62
|
+
const chipIcon = computed(() => {
|
|
63
|
+
if (status.value === "loading")
|
|
64
|
+
return "i-ph-spinner animate-spin text-neutral-400";
|
|
65
|
+
if (status.value === "unresolved")
|
|
66
|
+
return "i-ph-warning-circle text-amber-600";
|
|
67
|
+
return "i-ph-file-text text-neutral-500";
|
|
68
|
+
});
|
|
69
|
+
const BADGE_TONE_CLASSES = {
|
|
70
|
+
neutral: "bg-neutral-100 text-neutral-700",
|
|
71
|
+
warning: "bg-amber-100 text-amber-800",
|
|
72
|
+
danger: "bg-red-100 text-red-700"
|
|
73
|
+
};
|
|
74
|
+
const badgeClasses = computed(() => BADGE_TONE_CLASSES[meta.value?.badge?.tone ?? "neutral"]);
|
|
75
|
+
</script>
|
|
76
|
+
|
|
77
|
+
<template>
|
|
78
|
+
<component
|
|
79
|
+
:is="config.component"
|
|
80
|
+
v-if="config?.component"
|
|
81
|
+
:citation="citation"
|
|
82
|
+
:label="label"
|
|
83
|
+
:status="status"
|
|
84
|
+
:meta="meta"
|
|
85
|
+
/>
|
|
86
|
+
<span
|
|
87
|
+
v-else
|
|
88
|
+
ref="rootEl"
|
|
89
|
+
class="citation-ref"
|
|
90
|
+
>
|
|
91
|
+
<button
|
|
92
|
+
type="button"
|
|
93
|
+
class="citation-ref-anchor"
|
|
94
|
+
:class="{
|
|
95
|
+
'citation-ref-anchor--loading': status === 'loading',
|
|
96
|
+
'citation-ref-anchor--unresolved': status === 'unresolved'
|
|
97
|
+
}"
|
|
98
|
+
:aria-expanded="popoverOpen"
|
|
99
|
+
@click="togglePopover"
|
|
100
|
+
><span :class="chipIcon" class="citation-ref-icon" />{{ displayLabel }}</button>
|
|
101
|
+
|
|
102
|
+
<div
|
|
103
|
+
v-if="popoverOpen"
|
|
104
|
+
absolute bottom-full left-0 mb-6px z-50
|
|
105
|
+
w-300px
|
|
106
|
+
bg-white
|
|
107
|
+
border-0.5px border-solid border-neutral-300
|
|
108
|
+
rounded-12px p-12px
|
|
109
|
+
shadow-lg
|
|
110
|
+
cursor-default
|
|
111
|
+
flex="~ col" gap-8px
|
|
112
|
+
>
|
|
113
|
+
<template v-if="status === 'unresolved'">
|
|
114
|
+
<div flex items-start gap-6px>
|
|
115
|
+
<span i-ph-warning-circle text-14px text-amber-600 mt-2px flex-shrink-0 />
|
|
116
|
+
<p text-12px leading-16px text-neutral-700>
|
|
117
|
+
Não foi possível verificar esta referência. Ela pode não existir ou não estar disponível para o seu perfil.
|
|
118
|
+
</p>
|
|
119
|
+
</div>
|
|
120
|
+
<button
|
|
121
|
+
type="button"
|
|
122
|
+
self-start text-12px leading-16px text-neutral-700 underline cursor-pointer
|
|
123
|
+
@click="retry"
|
|
124
|
+
>
|
|
125
|
+
Tentar novamente
|
|
126
|
+
</button>
|
|
127
|
+
</template>
|
|
128
|
+
|
|
129
|
+
<template v-else-if="meta">
|
|
130
|
+
<div flex="~ col" gap-2px>
|
|
131
|
+
<p text-13px leading-18px font-580 text-neutral-900>
|
|
132
|
+
{{ meta.title }}
|
|
133
|
+
</p>
|
|
134
|
+
<p v-if="meta.sourceLabel || meta.identifier" text-12px leading-16px text-neutral-600>
|
|
135
|
+
{{ [meta.sourceLabel, meta.identifier].filter(Boolean).join(" \xB7 ") }}
|
|
136
|
+
</p>
|
|
137
|
+
<p v-if="formattedDate" text-12px leading-16px text-neutral-500>
|
|
138
|
+
{{ meta.dateLabel ? `${meta.dateLabel}: ` : "" }}{{ formattedDate }}
|
|
139
|
+
</p>
|
|
140
|
+
</div>
|
|
141
|
+
|
|
142
|
+
<span
|
|
143
|
+
v-if="meta.badge"
|
|
144
|
+
self-start px-6px py-1px rounded-4px text-11px leading-15px font-580
|
|
145
|
+
:class="badgeClasses"
|
|
146
|
+
>
|
|
147
|
+
{{ meta.badge.label }}
|
|
148
|
+
</span>
|
|
149
|
+
|
|
150
|
+
<p v-if="meta.snippet" text-12px leading-17px text-neutral-600 line-clamp-3>
|
|
151
|
+
{{ meta.snippet }}
|
|
152
|
+
</p>
|
|
153
|
+
|
|
154
|
+
<div v-if="canOpenDocument || safeSourceUrl" flex items-center gap-12px pt-2px>
|
|
155
|
+
<button
|
|
156
|
+
v-if="canOpenDocument"
|
|
157
|
+
type="button"
|
|
158
|
+
inline-flex items-center gap-4px
|
|
159
|
+
text-12px leading-16px font-580 text-neutral-900
|
|
160
|
+
cursor-pointer hover:underline
|
|
161
|
+
@click="openDocument"
|
|
162
|
+
>
|
|
163
|
+
<span i-ph-file-magnifying-glass text-13px />
|
|
164
|
+
Ver documento
|
|
165
|
+
</button>
|
|
166
|
+
<a
|
|
167
|
+
v-if="safeSourceUrl"
|
|
168
|
+
:href="safeSourceUrl"
|
|
169
|
+
target="_blank"
|
|
170
|
+
rel="noopener noreferrer"
|
|
171
|
+
inline-flex items-center gap-4px
|
|
172
|
+
text-12px leading-16px text-neutral-600
|
|
173
|
+
hover:underline
|
|
174
|
+
>
|
|
175
|
+
<span i-ph-arrow-square-out text-13px />
|
|
176
|
+
Abrir na fonte
|
|
177
|
+
</a>
|
|
178
|
+
</div>
|
|
179
|
+
</template>
|
|
180
|
+
</div>
|
|
181
|
+
</span>
|
|
182
|
+
</template>
|
|
183
|
+
|
|
184
|
+
<style scoped>
|
|
185
|
+
.citation-ref{display:inline;position:relative}.citation-ref-anchor{background:transparent;border:0;border-radius:5px;color:inherit;cursor:pointer;display:inline;font:inherit;margin:0 -2px;padding:1px 3px;text-decoration:underline;text-decoration-color:#a3a3a3;text-decoration-style:dotted;text-decoration-thickness:1px;text-underline-offset:3px;transition:background-color .15s ease,text-decoration-color .15s ease}.citation-ref-anchor:hover{background-color:#f5f5f5;text-decoration-color:#525252}.citation-ref-anchor--loading{cursor:default;opacity:.6;text-decoration-color:#d4d4d4}.citation-ref-anchor--unresolved{text-decoration-color:#f59e0b}.citation-ref-anchor--unresolved:hover{background-color:#fffbeb}.citation-ref-icon{display:inline-block;font-size:.85em;margin-right:.25em;vertical-align:-.08em}
|
|
186
|
+
</style>
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { CitationRef } from '../citation-links.js';
|
|
2
|
+
type __VLS_Props = {
|
|
3
|
+
citation: CitationRef;
|
|
4
|
+
/** Visible text when the citation came from a markdown link. */
|
|
5
|
+
label?: string;
|
|
6
|
+
};
|
|
7
|
+
declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
8
|
+
declare const _default: typeof __VLS_export;
|
|
9
|
+
export default _default;
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
declare const __VLS_export: import("vue").DefineComponent<{}, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
|
|
2
|
+
declare const _default: typeof __VLS_export;
|
|
3
|
+
export default _default;
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { useMagicKeys } from "@vueuse/core";
|
|
3
|
+
import { useEmbedConfig } from "../../embed/composables/embed-config";
|
|
4
|
+
import { useCitationPanel } from "../composables/citation-panel";
|
|
5
|
+
import { escapeCitationMarkup, locateTermClientSide, splitContentByOccurrences } from "../document-highlight";
|
|
6
|
+
import CitationHighlight from "./citation-highlight.vue";
|
|
7
|
+
const { isOpen, active, closeCitationDocument } = useCitationPanel();
|
|
8
|
+
const embedConfig = useEmbedConfig();
|
|
9
|
+
const config = computed(() => embedConfig?.citations.value);
|
|
10
|
+
const citationHighlightTag = "citation_highlight";
|
|
11
|
+
const markdownComponents = computed(() => ({
|
|
12
|
+
...embedConfig?.customComponents.value ?? {},
|
|
13
|
+
[citationHighlightTag]: CitationHighlight
|
|
14
|
+
}));
|
|
15
|
+
const markdownHtmlTags = computed(() => [
|
|
16
|
+
.../* @__PURE__ */ new Set([
|
|
17
|
+
...embedConfig?.customHtmlTags.value ?? [],
|
|
18
|
+
citationHighlightTag
|
|
19
|
+
])
|
|
20
|
+
]);
|
|
21
|
+
const isLoading = ref(false);
|
|
22
|
+
const loadFailed = ref(false);
|
|
23
|
+
const documentData = ref(null);
|
|
24
|
+
const occurrences = ref([]);
|
|
25
|
+
const contentEl = ref(null);
|
|
26
|
+
let loadToken = 0;
|
|
27
|
+
const { escape } = useMagicKeys();
|
|
28
|
+
watch(() => escape?.value, (pressed) => {
|
|
29
|
+
if (pressed && isOpen.value)
|
|
30
|
+
closeCitationDocument();
|
|
31
|
+
});
|
|
32
|
+
watch([isOpen, active], () => {
|
|
33
|
+
void load();
|
|
34
|
+
}, { immediate: true });
|
|
35
|
+
async function load() {
|
|
36
|
+
if (!isOpen.value || !active.value)
|
|
37
|
+
return;
|
|
38
|
+
const activeConfig = config.value;
|
|
39
|
+
if (!activeConfig?.getDocument)
|
|
40
|
+
return;
|
|
41
|
+
const token = ++loadToken;
|
|
42
|
+
const citation = active.value.citation;
|
|
43
|
+
isLoading.value = true;
|
|
44
|
+
loadFailed.value = false;
|
|
45
|
+
documentData.value = null;
|
|
46
|
+
occurrences.value = [];
|
|
47
|
+
try {
|
|
48
|
+
const loaded = await activeConfig.getDocument(citation);
|
|
49
|
+
if (token !== loadToken)
|
|
50
|
+
return;
|
|
51
|
+
documentData.value = loaded;
|
|
52
|
+
loadFailed.value = loaded === null;
|
|
53
|
+
const term = citation.locator?.term;
|
|
54
|
+
if (loaded && term) {
|
|
55
|
+
let located = null;
|
|
56
|
+
if (activeConfig.locate) {
|
|
57
|
+
try {
|
|
58
|
+
located = await activeConfig.locate(citation, term);
|
|
59
|
+
} catch {
|
|
60
|
+
located = null;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
if (token !== loadToken)
|
|
64
|
+
return;
|
|
65
|
+
occurrences.value = located ?? locateTermClientSide(loaded.content, term);
|
|
66
|
+
}
|
|
67
|
+
} catch {
|
|
68
|
+
if (token === loadToken)
|
|
69
|
+
loadFailed.value = true;
|
|
70
|
+
} finally {
|
|
71
|
+
if (token === loadToken)
|
|
72
|
+
isLoading.value = false;
|
|
73
|
+
}
|
|
74
|
+
if (token !== loadToken)
|
|
75
|
+
return;
|
|
76
|
+
await nextTick();
|
|
77
|
+
scrollToTargetOccurrence();
|
|
78
|
+
}
|
|
79
|
+
const highlightSegments = computed(() => {
|
|
80
|
+
if (!documentData.value)
|
|
81
|
+
return [];
|
|
82
|
+
return splitContentByOccurrences(documentData.value.content, occurrences.value);
|
|
83
|
+
});
|
|
84
|
+
const totalOccurrences = computed(() => highlightSegments.value.filter((segment) => segment.occurrence !== void 0).length);
|
|
85
|
+
const targetOccurrence = computed(() => {
|
|
86
|
+
if (totalOccurrences.value === 0)
|
|
87
|
+
return 0;
|
|
88
|
+
const wanted = active.value?.citation.locator?.occurrence ?? 1;
|
|
89
|
+
return Math.min(wanted, totalOccurrences.value);
|
|
90
|
+
});
|
|
91
|
+
const highlightedContent = computed(() => highlightSegments.value.map((segment) => {
|
|
92
|
+
if (segment.occurrence === void 0)
|
|
93
|
+
return escapeCitationMarkup(segment.text, citationHighlightTag, "document");
|
|
94
|
+
const target = segment.occurrence === targetOccurrence.value ? ' target="true"' : "";
|
|
95
|
+
const content = escapeCitationMarkup(segment.text, citationHighlightTag, "highlight");
|
|
96
|
+
return `<${citationHighlightTag} occurrence="${segment.occurrence}"${target}>${content}</${citationHighlightTag}>`;
|
|
97
|
+
}).join(""));
|
|
98
|
+
const termNotFound = computed(() => Boolean(
|
|
99
|
+
!isLoading.value && documentData.value && active.value?.citation.locator?.term && totalOccurrences.value === 0
|
|
100
|
+
));
|
|
101
|
+
function scrollToTargetOccurrence() {
|
|
102
|
+
if (targetOccurrence.value === 0)
|
|
103
|
+
return;
|
|
104
|
+
const target = contentEl.value?.querySelector(`[data-occurrence="${targetOccurrence.value}"]`);
|
|
105
|
+
target?.scrollIntoView({ block: "center" });
|
|
106
|
+
}
|
|
107
|
+
const meta = computed(() => active.value?.meta ?? null);
|
|
108
|
+
const subtitle = computed(() => [meta.value?.sourceLabel, meta.value?.identifier].filter(Boolean).join(" \xB7 "));
|
|
109
|
+
const safeSourceUrl = computed(() => {
|
|
110
|
+
const url = meta.value?.url;
|
|
111
|
+
return url && /^https?:\/\//i.test(url) ? url : null;
|
|
112
|
+
});
|
|
113
|
+
</script>
|
|
114
|
+
|
|
115
|
+
<template>
|
|
116
|
+
<Transition name="slide-panel">
|
|
117
|
+
<div
|
|
118
|
+
v-if="isOpen && active"
|
|
119
|
+
class="h-full w-full flex flex-col bg-neutral-50 b-l='.5px neutral-300'"
|
|
120
|
+
style="box-shadow: 0px 12px 20px 0px rgba(163, 163, 163, 0.06);"
|
|
121
|
+
>
|
|
122
|
+
<div
|
|
123
|
+
relative flex items-start gap-8px
|
|
124
|
+
pl-18px pr-56px py-12px
|
|
125
|
+
flex-shrink-0
|
|
126
|
+
>
|
|
127
|
+
<span i-ph-file-text text-16px text-neutral-600 mt-2px flex-shrink-0 />
|
|
128
|
+
<div flex="~ col" gap-2px min-w-0>
|
|
129
|
+
<p
|
|
130
|
+
text-13px font-580 text-secondary leading-17px
|
|
131
|
+
:title="meta?.title ?? 'Documento'"
|
|
132
|
+
>
|
|
133
|
+
{{ meta?.title ?? "Documento" }}
|
|
134
|
+
</p>
|
|
135
|
+
<p v-if="subtitle" text-12px leading-16px text-neutral-500 truncate>
|
|
136
|
+
{{ subtitle }}
|
|
137
|
+
</p>
|
|
138
|
+
</div>
|
|
139
|
+
|
|
140
|
+
<div class="absolute top-8px right-12px flex items-center gap-4px">
|
|
141
|
+
<a
|
|
142
|
+
v-if="safeSourceUrl"
|
|
143
|
+
:href="safeSourceUrl"
|
|
144
|
+
target="_blank"
|
|
145
|
+
rel="noopener noreferrer"
|
|
146
|
+
title="Abrir na fonte original"
|
|
147
|
+
class="flex items-center justify-center w-28px h-28px rounded-8px hover:bg-neutral-200 transition-colors"
|
|
148
|
+
>
|
|
149
|
+
<span class="i-ph-arrow-square-out text-16px text-gray-700" />
|
|
150
|
+
</a>
|
|
151
|
+
<button
|
|
152
|
+
type="button"
|
|
153
|
+
title="Fechar"
|
|
154
|
+
class="flex items-center justify-center w-28px h-28px rounded-8px hover:bg-neutral-200 transition-colors"
|
|
155
|
+
@click="closeCitationDocument"
|
|
156
|
+
>
|
|
157
|
+
<span class="i-ph-x text-16px text-gray-700" />
|
|
158
|
+
</button>
|
|
159
|
+
</div>
|
|
160
|
+
</div>
|
|
161
|
+
|
|
162
|
+
<div
|
|
163
|
+
v-if="documentData?.truncated || termNotFound"
|
|
164
|
+
flex="~ col" gap-6px px-18px pb-8px flex-shrink-0
|
|
165
|
+
>
|
|
166
|
+
<div
|
|
167
|
+
v-if="termNotFound"
|
|
168
|
+
flex items-start gap-6px
|
|
169
|
+
px-10px py-8px rounded-8px
|
|
170
|
+
bg-amber-50 border-0.5px border-solid border-amber-200
|
|
171
|
+
>
|
|
172
|
+
<span i-ph-warning-circle text-13px text-amber-600 mt-1px flex-shrink-0 />
|
|
173
|
+
<p text-12px leading-16px text-amber-800>
|
|
174
|
+
Trecho citado não localizado neste documento.
|
|
175
|
+
</p>
|
|
176
|
+
</div>
|
|
177
|
+
<div
|
|
178
|
+
v-if="documentData?.truncated"
|
|
179
|
+
flex items-start gap-6px
|
|
180
|
+
px-10px py-8px rounded-8px
|
|
181
|
+
bg-neutral-100 border-0.5px border-solid border-neutral-200
|
|
182
|
+
>
|
|
183
|
+
<span i-ph-info text-13px text-neutral-500 mt-1px flex-shrink-0 />
|
|
184
|
+
<p text-12px leading-16px text-neutral-600>
|
|
185
|
+
Documento exibido parcialmente.
|
|
186
|
+
</p>
|
|
187
|
+
</div>
|
|
188
|
+
</div>
|
|
189
|
+
|
|
190
|
+
<div
|
|
191
|
+
v-if="isLoading"
|
|
192
|
+
flex-1 flex items-center justify-center
|
|
193
|
+
>
|
|
194
|
+
<span i-ph-spinner animate-spin text-24px text-neutral-400 />
|
|
195
|
+
</div>
|
|
196
|
+
|
|
197
|
+
<div
|
|
198
|
+
v-else-if="loadFailed"
|
|
199
|
+
flex-1 flex="~ col" items-center justify-center gap-4px px-24px
|
|
200
|
+
>
|
|
201
|
+
<span i-ph-file-x text-24px text-neutral-400 />
|
|
202
|
+
<p text-13px leading-18px text-neutral-700 text-center>
|
|
203
|
+
Documento não disponível.
|
|
204
|
+
</p>
|
|
205
|
+
<p text-12px leading-16px text-neutral-500 text-center>
|
|
206
|
+
Ele pode não existir ou não estar acessível para o seu perfil.
|
|
207
|
+
</p>
|
|
208
|
+
</div>
|
|
209
|
+
|
|
210
|
+
<div
|
|
211
|
+
v-else-if="documentData"
|
|
212
|
+
ref="contentEl"
|
|
213
|
+
flex-1 min-h-0 overflow-y-auto
|
|
214
|
+
bg-muted p-16px
|
|
215
|
+
>
|
|
216
|
+
<div
|
|
217
|
+
min-h-full bg-white
|
|
218
|
+
b=".5px border-subtle"
|
|
219
|
+
rounded-12px p-24px
|
|
220
|
+
>
|
|
221
|
+
<ClientOnly>
|
|
222
|
+
<AppMarkdownRender
|
|
223
|
+
:content="highlightedContent"
|
|
224
|
+
:custom-components="markdownComponents"
|
|
225
|
+
:custom-html-tags="markdownHtmlTags"
|
|
226
|
+
:batch-rendering="false"
|
|
227
|
+
:max-live-nodes="0"
|
|
228
|
+
:render-code-blocks-as-pre="true"
|
|
229
|
+
:typewriter="false"
|
|
230
|
+
text-neutral-800
|
|
231
|
+
/>
|
|
232
|
+
</ClientOnly>
|
|
233
|
+
</div>
|
|
234
|
+
</div>
|
|
235
|
+
</div>
|
|
236
|
+
</Transition>
|
|
237
|
+
</template>
|
|
238
|
+
|
|
239
|
+
<style scoped>
|
|
240
|
+
.slide-panel-enter-active,.slide-panel-leave-active{transition:all .2s ease}.slide-panel-enter-from,.slide-panel-leave-to{opacity:0;transform:translateX(100%)}
|
|
241
|
+
</style>
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
declare const __VLS_export: import("vue").DefineComponent<{}, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
|
|
2
|
+
declare const _default: typeof __VLS_export;
|
|
3
|
+
export default _default;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { CustomMarkdownNode } from '../../rendering/markdown/custom-components.js';
|
|
2
|
+
type __VLS_Props = {
|
|
3
|
+
node: CustomMarkdownNode;
|
|
4
|
+
occurrence: string;
|
|
5
|
+
target?: boolean | string;
|
|
6
|
+
};
|
|
7
|
+
declare var __VLS_1: {};
|
|
8
|
+
type __VLS_Slots = {} & {
|
|
9
|
+
default?: (props: typeof __VLS_1) => any;
|
|
10
|
+
};
|
|
11
|
+
declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
12
|
+
declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
|
|
13
|
+
declare const _default: typeof __VLS_export;
|
|
14
|
+
export default _default;
|
|
15
|
+
type __VLS_WithSlots<T, S> = T & {
|
|
16
|
+
new (): {
|
|
17
|
+
$slots: S;
|
|
18
|
+
};
|
|
19
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
const props = defineProps({
|
|
3
|
+
node: { type: Object, required: true },
|
|
4
|
+
occurrence: { type: String, required: true },
|
|
5
|
+
target: { type: [Boolean, String], required: false }
|
|
6
|
+
});
|
|
7
|
+
const isTarget = computed(() => props.target === true || props.target === "true");
|
|
8
|
+
</script>
|
|
9
|
+
|
|
10
|
+
<template>
|
|
11
|
+
<mark
|
|
12
|
+
:data-occurrence="occurrence"
|
|
13
|
+
rounded-2px
|
|
14
|
+
:class="isTarget ? 'bg-amber-300' : 'bg-amber-100'"
|
|
15
|
+
><slot /></mark>
|
|
16
|
+
</template>
|