@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.
Files changed (50) hide show
  1. package/README.md +26 -0
  2. package/dist/module.json +1 -1
  3. package/dist/module.mjs +4 -3
  4. package/dist/runtime/citations/citation-links.d.ts +32 -0
  5. package/dist/runtime/citations/citation-links.js +56 -0
  6. package/dist/runtime/citations/components/chat-citation.d.vue.ts +9 -0
  7. package/dist/runtime/citations/components/chat-citation.vue +186 -0
  8. package/dist/runtime/citations/components/chat-citation.vue.d.ts +9 -0
  9. package/dist/runtime/citations/components/citation-document-panel.d.vue.ts +3 -0
  10. package/dist/runtime/citations/components/citation-document-panel.vue +241 -0
  11. package/dist/runtime/citations/components/citation-document-panel.vue.d.ts +3 -0
  12. package/dist/runtime/citations/components/citation-highlight.d.vue.ts +19 -0
  13. package/dist/runtime/citations/components/citation-highlight.vue +16 -0
  14. package/dist/runtime/citations/components/citation-highlight.vue.d.ts +19 -0
  15. package/dist/runtime/citations/composables/citation-panel.d.ts +8 -0
  16. package/dist/runtime/citations/composables/citation-panel.js +17 -0
  17. package/dist/runtime/citations/composables/citations.d.ts +17 -0
  18. package/dist/runtime/citations/composables/citations.js +44 -0
  19. package/dist/runtime/citations/document-highlight.d.ts +14 -0
  20. package/dist/runtime/citations/document-highlight.js +43 -0
  21. package/dist/runtime/citations/types.d.ts +57 -0
  22. package/dist/runtime/citations/types.js +0 -0
  23. package/dist/runtime/conversations/components/chat-conversation-list.vue +2 -2
  24. package/dist/runtime/embed/components/chat-embed-inner.vue +15 -0
  25. package/dist/runtime/embed/components/chat-embed.vue +2 -0
  26. package/dist/runtime/embed/components/configuration/chat-configuration-knowledge-sources-tab.d.vue.ts +2 -2
  27. package/dist/runtime/embed/components/configuration/chat-configuration-knowledge-sources-tab.vue.d.ts +2 -2
  28. package/dist/runtime/embed/components/meistrari-chat-embed.vue +3 -0
  29. package/dist/runtime/embed/components/mobile/chat-mobile-drawer.vue +2 -2
  30. package/dist/runtime/embed/components/mobile/chat-mobile-shell.d.vue.ts +2 -2
  31. package/dist/runtime/embed/components/mobile/chat-mobile-shell.vue.d.ts +2 -2
  32. package/dist/runtime/embed/composables/embed-config.d.ts +3 -0
  33. package/dist/runtime/embed/composables/embed-config.js +9 -0
  34. package/dist/runtime/embed/types.d.ts +12 -1
  35. package/dist/runtime/files/components/chat-vault-link.vue +13 -0
  36. package/dist/runtime/files/content-detection.d.ts +9 -1
  37. package/dist/runtime/files/content-detection.js +29 -8
  38. package/dist/runtime/messages/components/chat-message-bubble.vue +7 -3
  39. package/dist/runtime/messages/components/reasoning/chat-reasoning-message.vue +22 -266
  40. package/dist/runtime/messages/components/reasoning/chat-reasoning-thought.vue +2 -3
  41. package/dist/runtime/messages/tool-registry.d.ts +15 -0
  42. package/dist/runtime/messages/tool-registry.js +138 -0
  43. package/dist/runtime/types/citations.d.ts +1 -0
  44. package/dist/runtime/types/citations.js +0 -0
  45. package/dist/runtime/types/embed.d.ts +1 -0
  46. package/package.json +1 -1
  47. package/dist/runtime/messages/composables/tool-formatting.d.ts +0 -5
  48. package/dist/runtime/messages/composables/tool-formatting.js +0 -61
  49. package/dist/runtime/shared/composables/verb-conjugation.d.ts +0 -7
  50. 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
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@meistrari/chat-nuxt",
3
3
  "configKey": "chatNuxt",
4
- "version": "3.7.0",
4
+ "version": "3.9.0",
5
5
  "builder": {
6
6
  "@nuxt/module-builder": "1.0.2",
7
7
  "unbuild": "unknown"
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>