@meistrari/minuta-nuxt 0.1.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 (45) hide show
  1. package/README.md +106 -0
  2. package/dist/module.cjs +5 -0
  3. package/dist/module.d.mts +18 -0
  4. package/dist/module.d.ts +18 -0
  5. package/dist/module.json +9 -0
  6. package/dist/module.mjs +45 -0
  7. package/dist/runtime/components/MinutaAnnotationsGutter.vue +425 -0
  8. package/dist/runtime/components/MinutaChatEditCard.vue +124 -0
  9. package/dist/runtime/components/MinutaChromeCanvas.vue +549 -0
  10. package/dist/runtime/components/MinutaChromeEditor.vue +100 -0
  11. package/dist/runtime/components/MinutaDocumentViewer.vue +90 -0
  12. package/dist/runtime/components/MinutaDocxEditor.vue +1158 -0
  13. package/dist/runtime/components/MinutaEmbed.vue +783 -0
  14. package/dist/runtime/components/MinutaIcon.vue +53 -0
  15. package/dist/runtime/composables/annotationAnchors.d.ts +73 -0
  16. package/dist/runtime/composables/annotationAnchors.js +243 -0
  17. package/dist/runtime/composables/chatEditOps.d.ts +37 -0
  18. package/dist/runtime/composables/chatEditOps.js +61 -0
  19. package/dist/runtime/composables/docxExport.d.ts +24 -0
  20. package/dist/runtime/composables/docxExport.js +1292 -0
  21. package/dist/runtime/composables/editorExtensions.d.ts +56 -0
  22. package/dist/runtime/composables/editorExtensions.js +466 -0
  23. package/dist/runtime/composables/paginationPlugin.d.ts +126 -0
  24. package/dist/runtime/composables/paginationPlugin.js +421 -0
  25. package/dist/runtime/composables/useMinutaApi.d.ts +112 -0
  26. package/dist/runtime/composables/useMinutaApi.js +68 -0
  27. package/dist/runtime/composables/vaultImages.d.ts +12 -0
  28. package/dist/runtime/composables/vaultImages.js +85 -0
  29. package/dist/runtime/composables/versionDiff.d.ts +34 -0
  30. package/dist/runtime/composables/versionDiff.js +62 -0
  31. package/dist/runtime/server/api/minuta/[...path].d.ts +2 -0
  32. package/dist/runtime/server/api/minuta/[...path].js +6 -0
  33. package/dist/runtime/server/api/minuta/index.d.ts +2 -0
  34. package/dist/runtime/server/api/minuta/index.js +3 -0
  35. package/dist/runtime/server/utils/minuta-proxy.d.ts +10 -0
  36. package/dist/runtime/server/utils/minuta-proxy.js +63 -0
  37. package/dist/runtime/utils/footnoteHtml.d.ts +26 -0
  38. package/dist/runtime/utils/footnoteHtml.js +117 -0
  39. package/dist/runtime/utils/pocKeepWithNext.d.ts +44 -0
  40. package/dist/runtime/utils/pocKeepWithNext.js +9 -0
  41. package/dist/runtime/utils/sanitizeRichHtml.d.ts +10 -0
  42. package/dist/runtime/utils/sanitizeRichHtml.js +73 -0
  43. package/dist/types.d.mts +7 -0
  44. package/dist/types.d.ts +7 -0
  45. package/package.json +100 -0
@@ -0,0 +1,421 @@
1
+ import { Plugin, PluginKey } from "@tiptap/pm/state";
2
+ import { Decoration, DecorationSet } from "@tiptap/pm/view";
3
+ import { sanitizeRichHtml } from "../utils/sanitizeRichHtml.js";
4
+ import { sanitizeFootnoteHtml } from "../utils/footnoteHtml.js";
5
+ import { shouldMoveBreakToHeading } from "../utils/pocKeepWithNext.js";
6
+ export const pocPageKey = new PluginKey("pocPagination");
7
+ export const PAGINATE_META = "poc-paginate";
8
+ export function pocChromeFlowHeight(cfg) {
9
+ return cfg.margins.bottom + cfg.gap + cfg.margins.top;
10
+ }
11
+ function editHint(part) {
12
+ return (event) => {
13
+ event.preventDefault();
14
+ event.stopPropagation();
15
+ window.dispatchEvent(new CustomEvent("poc:edit-chrome", { detail: { part } }));
16
+ };
17
+ }
18
+ export function isPocChromeCanvas(html) {
19
+ return !!html && html.includes("data-el=");
20
+ }
21
+ function chromeContentEl(html, cfg) {
22
+ const clean = sanitizeRichHtml(html);
23
+ if (isPocChromeCanvas(clean)) {
24
+ const canvas = document.createElement("div");
25
+ canvas.className = "poc-chrome-canvas";
26
+ canvas.style.cssText = `position:absolute;top:0;left:${-cfg.margins.left}px;width:794px;height:100%;pointer-events:none;`;
27
+ canvas.innerHTML = clean;
28
+ return canvas;
29
+ }
30
+ const text = document.createElement("div");
31
+ text.className = "poc-chrome-text";
32
+ text.innerHTML = clean;
33
+ return text;
34
+ }
35
+ export function buildPocHeaderEl(cfg) {
36
+ const header = document.createElement("div");
37
+ header.className = "poc-page-headerarea";
38
+ header.style.height = `${cfg.margins.top}px`;
39
+ header.style.position = "relative";
40
+ header.style.justifyContent = cfg.headerPosition === "center" ? "center" : cfg.headerPosition === "right" ? "flex-end" : "flex-start";
41
+ header.title = "Clique para editar o cabe\xE7alho";
42
+ header.addEventListener("click", editHint("header"));
43
+ if (cfg.headerScope === "first")
44
+ return header;
45
+ if (cfg.headerLogoUrl) {
46
+ const img = document.createElement("img");
47
+ img.src = cfg.headerLogoUrl;
48
+ img.style.maxHeight = `${Math.max(24, cfg.margins.top - 36)}px`;
49
+ img.style.maxWidth = "50%";
50
+ img.style.display = "inline-block";
51
+ header.appendChild(img);
52
+ }
53
+ const content = chromeContentEl(cfg.headerText, cfg);
54
+ if (content.className === "poc-chrome-text" && !content.innerHTML && !cfg.headerLogoUrl)
55
+ content.innerHTML = " ";
56
+ header.appendChild(content);
57
+ return header;
58
+ }
59
+ export function buildPocFooterEl(cfg, pageNo, total) {
60
+ const footer = document.createElement("div");
61
+ footer.className = "poc-page-footerarea";
62
+ footer.style.height = `${cfg.margins.bottom}px`;
63
+ footer.style.position = "relative";
64
+ footer.title = "Clique para editar o rodap\xE9";
65
+ footer.addEventListener("click", editHint("footer"));
66
+ const showCustom = !(cfg.footerScope === "first" && pageNo !== 1);
67
+ const custom = showCustom ? chromeContentEl(cfg.footerText, cfg) : document.createElement("div");
68
+ if (!showCustom)
69
+ custom.className = "poc-chrome-text";
70
+ footer.appendChild(custom);
71
+ if (cfg.showPageNumber !== false) {
72
+ const pageLabel = document.createElement("span");
73
+ pageLabel.textContent = `P\xE1gina ${pageNo} de ${total}`;
74
+ footer.appendChild(pageLabel);
75
+ }
76
+ if (custom.className === "poc-chrome-canvas")
77
+ footer.style.justifyContent = "flex-end";
78
+ return footer;
79
+ }
80
+ export function buildPocNotesEl(notes) {
81
+ const box = document.createElement("div");
82
+ box.className = "poc-page-notes";
83
+ for (const note of notes) {
84
+ const line = document.createElement("p");
85
+ line.className = "poc-page-note";
86
+ if (note.size)
87
+ line.style.fontSize = `${note.size}pt`;
88
+ line.innerHTML = `<sup>${note.n}</sup> ${sanitizeFootnoteHtml(note.text)}`;
89
+ box.appendChild(line);
90
+ }
91
+ return box;
92
+ }
93
+ function buildChrome(cfg, brk, total) {
94
+ const chrome = document.createElement("div");
95
+ chrome.className = "poc-page-chrome";
96
+ chrome.contentEditable = "false";
97
+ chrome.style.height = `${brk.filler + (brk.notesH ?? 0) + pocChromeFlowHeight(cfg)}px`;
98
+ const filler = document.createElement("div");
99
+ filler.style.height = `${brk.filler}px`;
100
+ chrome.appendChild(filler);
101
+ if (brk.notes?.length)
102
+ chrome.appendChild(buildPocNotesEl(brk.notes));
103
+ chrome.appendChild(buildPocFooterEl(cfg, brk.pageNo, total));
104
+ const band = document.createElement("div");
105
+ band.className = "poc-page-gapband";
106
+ band.style.height = `${cfg.gap}px`;
107
+ band.style.marginLeft = `-${cfg.margins.left}px`;
108
+ band.style.marginRight = `-${cfg.margins.right}px`;
109
+ chrome.appendChild(band);
110
+ chrome.appendChild(buildPocHeaderEl(cfg));
111
+ return chrome;
112
+ }
113
+ function absTop(el, sheet) {
114
+ let top = 0;
115
+ let node = el;
116
+ while (node && node !== sheet) {
117
+ top += node.offsetTop;
118
+ node = node.offsetParent;
119
+ }
120
+ return top;
121
+ }
122
+ const HEADING_TAGS = /* @__PURE__ */ new Set(["H1", "H2", "H3", "H4"]);
123
+ function pageSafetyPx(contentHeight) {
124
+ return Math.max(32, Math.round(contentHeight * 0.08));
125
+ }
126
+ const NOTES_RULE_PX = 4;
127
+ function createNotesMeasurer(sheet, contentWidth) {
128
+ const host = document.createElement("div");
129
+ host.className = "poc-page-notes poc-notes-measure";
130
+ host.style.cssText = "position:absolute;left:0;top:0;visibility:hidden;pointer-events:none;";
131
+ host.style.width = `${contentWidth}px`;
132
+ sheet.appendChild(host);
133
+ const cache = /* @__PURE__ */ new Map();
134
+ return {
135
+ /** altura total (filete + notas) de um conjunto de notas */
136
+ height(notes) {
137
+ if (!notes.length)
138
+ return 0;
139
+ const key = notes.map((n) => `${n.n}:${n.size ?? ""}:${n.text.length}`).join("|");
140
+ const hit = cache.get(key);
141
+ if (hit != null)
142
+ return hit;
143
+ host.replaceChildren(...Array.from(buildPocNotesEl(notes).childNodes));
144
+ const value = host.offsetHeight + NOTES_RULE_PX;
145
+ cache.set(key, value);
146
+ return value;
147
+ },
148
+ dispose() {
149
+ host.remove();
150
+ }
151
+ };
152
+ }
153
+ export function computePocBreaks(view, cfg, sheet) {
154
+ const bandH = cfg.pageHeight - cfg.margins.top - cfg.margins.bottom;
155
+ const safety = pageSafetyPx(bandH);
156
+ const contentH = bandH - safety;
157
+ if (contentH <= 40)
158
+ return { breaks: [], total: 1, markers: [], lastNotes: [] };
159
+ const chromes = Array.from(sheet.querySelectorAll(".poc-page-chrome")).map((el) => ({ top: absTop(el, sheet), height: el.offsetHeight })).sort((a, b) => a.top - b.top);
160
+ const chromeAbove = (top) => {
161
+ let sum = 0;
162
+ for (const c of chromes) {
163
+ if (c.top < top)
164
+ sum += c.height;
165
+ else break;
166
+ }
167
+ return sum;
168
+ };
169
+ const items = [];
170
+ const baseY = cfg.margins.top;
171
+ let noteCounter = 0;
172
+ view.state.doc.forEach((block, blockOffset) => {
173
+ if (block.type.name !== "minutaBlock") {
174
+ return;
175
+ }
176
+ block.forEach((child, childOffset) => {
177
+ const pos = blockOffset + 1 + childOffset;
178
+ const dom = view.nodeDOM(pos);
179
+ if (!(dom instanceof HTMLElement))
180
+ return;
181
+ const top = absTop(dom, sheet) - baseY - chromeAbove(absTop(dom, sheet));
182
+ const notes = [];
183
+ child.descendants((inner) => {
184
+ if (inner.type.name === "footnoteRef" && inner.attrs.text) {
185
+ noteCounter += 1;
186
+ notes.push({ n: noteCounter, text: String(inner.attrs.text), size: inner.attrs.size ?? null });
187
+ }
188
+ return true;
189
+ });
190
+ items.push({
191
+ pos,
192
+ size: child.nodeSize,
193
+ top,
194
+ height: dom.offsetHeight,
195
+ notes,
196
+ // margem superior conta como parte da página que o item abre —
197
+ // senão a "página" do preview fica mais alta que um A4 real e
198
+ // o DOCX (página física) divergiria
199
+ marginTop: Number.parseFloat(getComputedStyle(dom).marginTop) || 0,
200
+ heading: HEADING_TAGS.has(dom.tagName),
201
+ marker: (dom.textContent || "").trim() === "PAGEBREAK",
202
+ // quebra que cai no 1º filho de um bloco vai ANTES do bloco
203
+ // (o chrome fora do div do bloco — senão o outline/label de
204
+ // proveniência "vaza" para a página anterior)
205
+ blockStartPos: childOffset === 0 ? blockOffset : null
206
+ });
207
+ });
208
+ });
209
+ if (!items.length)
210
+ return { breaks: [], total: 1, markers: [], lastNotes: [] };
211
+ const markers = items.filter((i) => i.marker).map((i) => ({ from: i.pos, to: i.pos + i.size }));
212
+ const hasNotes = items.some((i) => i.notes.length);
213
+ const measurer = hasNotes ? createNotesMeasurer(sheet, sheet.clientWidth - cfg.margins.left - cfg.margins.right) : null;
214
+ const breaks = [];
215
+ let pageStart = 0;
216
+ let pageNo = 1;
217
+ let forcedBreakPending = false;
218
+ let pageNotes = [];
219
+ const notesH = (notes) => measurer && notes.length ? measurer.height(notes) : 0;
220
+ const closePage = (pos, filler) => {
221
+ const height = notesH(pageNotes);
222
+ breaks.push({ pos, filler: Math.max(0, filler - height), pageNo, notes: pageNotes, notesH: height });
223
+ pageNo++;
224
+ pageNotes = [];
225
+ };
226
+ const maxSteps = items.length * 4 + 64;
227
+ let steps = 0;
228
+ for (let i = 0; i < items.length; i++) {
229
+ if (++steps > maxSteps) {
230
+ console.warn("[poc] pagina\xE7\xE3o interrompida: la\xE7o sem progresso", { items: items.length, breaks: breaks.length });
231
+ break;
232
+ }
233
+ const item = items[i];
234
+ if (item.marker) {
235
+ forcedBreakPending = true;
236
+ continue;
237
+ }
238
+ const fullH = contentH + safety;
239
+ const itemEnd = item.top + item.height;
240
+ const capacity = contentH - notesH([...pageNotes, ...item.notes]);
241
+ if (forcedBreakPending && item.top > pageStart) {
242
+ const startWithMargin = item.top - item.marginTop;
243
+ closePage(item.blockStartPos ?? item.pos, fullH - (startWithMargin - pageStart));
244
+ pageStart = startWithMargin;
245
+ forcedBreakPending = false;
246
+ } else if (item.top > pageStart && itemEnd - pageStart > capacity) {
247
+ let breakIndex = i;
248
+ if (shouldMoveBreakToHeading({ heading: items[i - 1], item, pageStart, capacity }))
249
+ breakIndex = i - 1;
250
+ const target = items[breakIndex];
251
+ const startWithMargin = target.top - target.marginTop;
252
+ const moved = [];
253
+ for (let k = breakIndex; k < i; k++) {
254
+ for (const note of items[k].notes) {
255
+ const at = pageNotes.indexOf(note);
256
+ if (at >= 0)
257
+ pageNotes.splice(at, 1);
258
+ moved.push(note);
259
+ }
260
+ }
261
+ closePage(target.blockStartPos ?? target.pos, fullH - (startWithMargin - pageStart));
262
+ pageNotes = moved;
263
+ pageStart = startWithMargin;
264
+ if (breakIndex < i) {
265
+ i = breakIndex;
266
+ continue;
267
+ }
268
+ }
269
+ forcedBreakPending = false;
270
+ pageNotes.push(...item.notes);
271
+ while (item.top + item.height - pageStart > contentH) {
272
+ pageStart += contentH;
273
+ pageNo++;
274
+ }
275
+ }
276
+ measurer?.dispose();
277
+ return { breaks, total: pageNo, markers, lastNotes: pageNotes };
278
+ }
279
+ function pocStyleProp(style, prop) {
280
+ if (!style)
281
+ return void 0;
282
+ const match = style.match(new RegExp(`(?:^|;)\\s*${prop}\\s*:\\s*([^;]+)`, "i"));
283
+ return match?.[1]?.trim();
284
+ }
285
+ export function isPocFlexRow(node) {
286
+ if (node.type.name !== "styledDiv")
287
+ return false;
288
+ const display = pocStyleProp(node.attrs.style, "display");
289
+ if (display !== "flex" && display !== "table")
290
+ return false;
291
+ if (pocStyleProp(node.attrs.style, "flex-direction") === "column")
292
+ return false;
293
+ return node.childCount > 1;
294
+ }
295
+ export function pocNodeHasVisibleContent(node) {
296
+ if (node.textContent.trim().length > 0)
297
+ return true;
298
+ let found = false;
299
+ const checkIsland = (island) => {
300
+ const html = island.attrs.html || "";
301
+ if (/<img[^>]*src\s*=\s*"[^"]+"/i.test(html) || html.includes("<svg"))
302
+ found = true;
303
+ };
304
+ if (node.type.name === "htmlIsland")
305
+ checkIsland(node);
306
+ node.descendants((child) => {
307
+ if (found)
308
+ return false;
309
+ if (child.type.name === "htmlIsland")
310
+ checkIsland(child);
311
+ return !found;
312
+ });
313
+ return found;
314
+ }
315
+ function paintsSomething(value) {
316
+ if (!value)
317
+ return false;
318
+ const v = value.trim().toLowerCase();
319
+ if (!v || /\b(?:none|hidden|transparent)\b/.test(v))
320
+ return false;
321
+ const width = v.match(/(?:^|\s)(\d*\.?\d+)(?:px|pt|em|rem|cm|mm|in)?(?=\s|$)/);
322
+ return !(width && Number.parseFloat(width[1]) === 0);
323
+ }
324
+ export function pocNodePaintsBox(node) {
325
+ const style = node.attrs?.style ?? null;
326
+ if (!style)
327
+ return false;
328
+ const props = ["border", "border-top", "border-right", "border-bottom", "border-left", "background", "background-color"];
329
+ return props.some((prop) => paintsSomething(pocStyleProp(style, prop)));
330
+ }
331
+ export function pocLoneCellIndex(row) {
332
+ if (pocNodePaintsBox(row))
333
+ return null;
334
+ let found = -1;
335
+ for (let i = 0; i < row.childCount; i++) {
336
+ const child = row.child(i);
337
+ if (pocNodePaintsBox(child))
338
+ return null;
339
+ if (pocNodeHasVisibleContent(child)) {
340
+ if (found !== -1)
341
+ return null;
342
+ found = i;
343
+ }
344
+ }
345
+ return found === -1 ? null : found;
346
+ }
347
+ export function computePocLoneRows(doc) {
348
+ const out = [];
349
+ doc.descendants((node, pos) => {
350
+ if (!isPocFlexRow(node))
351
+ return true;
352
+ const index = pocLoneCellIndex(node);
353
+ if (index == null)
354
+ return true;
355
+ let offset = pos + 1;
356
+ for (let i = 0; i < index; i++)
357
+ offset += node.child(i).nodeSize;
358
+ out.push({
359
+ rowFrom: pos,
360
+ rowTo: pos + node.nodeSize,
361
+ cellFrom: offset,
362
+ cellTo: offset + node.child(index).nodeSize
363
+ });
364
+ return true;
365
+ });
366
+ return out;
367
+ }
368
+ export function breaksEqual(a, b) {
369
+ if (a.length !== b.length)
370
+ return false;
371
+ for (let i = 0; i < a.length; i++) {
372
+ if (a[i].pos !== b[i].pos || Math.abs(a[i].filler - b[i].filler) > 1)
373
+ return false;
374
+ if ((a[i].notes?.length ?? 0) !== (b[i].notes?.length ?? 0))
375
+ return false;
376
+ }
377
+ return true;
378
+ }
379
+ export function PocPaginationPlugin(cfg) {
380
+ return new Plugin({
381
+ key: pocPageKey,
382
+ state: {
383
+ init: () => ({ breaks: [], total: 1, markers: [] }),
384
+ apply(tr, value) {
385
+ const next = tr.getMeta(PAGINATE_META);
386
+ if (next)
387
+ return { markers: value.markers, ...next };
388
+ if (tr.docChanged) {
389
+ return {
390
+ ...value,
391
+ breaks: value.breaks.map((b) => ({ ...b, pos: tr.mapping.map(b.pos) })),
392
+ markers: value.markers.map((m) => ({ from: tr.mapping.map(m.from), to: tr.mapping.map(m.to) }))
393
+ };
394
+ }
395
+ return value;
396
+ }
397
+ },
398
+ props: {
399
+ decorations(state) {
400
+ const pag = pocPageKey.getState(state);
401
+ const decorations = (pag?.breaks ?? []).map(
402
+ (brk) => Decoration.widget(brk.pos, () => buildChrome(cfg, brk, pag.total), {
403
+ side: -1,
404
+ key: `chrome-${brk.pageNo}-${Math.round(brk.filler)}-${brk.notes?.length ?? 0}-${pag.total}-v${cfg.version ?? 0}`
405
+ })
406
+ );
407
+ for (const marker of pag?.markers ?? []) {
408
+ if (marker.to > marker.from && marker.to <= state.doc.content.size)
409
+ decorations.push(Decoration.node(marker.from, marker.to, { class: "poc-pagebreak-marker" }));
410
+ }
411
+ for (const lone of computePocLoneRows(state.doc)) {
412
+ decorations.push(Decoration.node(lone.rowFrom, lone.rowTo, { class: "poc-lone-row" }));
413
+ decorations.push(Decoration.node(lone.cellFrom, lone.cellTo, { class: "poc-lone-cell" }));
414
+ }
415
+ if (!decorations.length)
416
+ return null;
417
+ return DecorationSet.create(state.doc, decorations);
418
+ }
419
+ }
420
+ });
421
+ }
@@ -0,0 +1,112 @@
1
+ export interface MinutaFile {
2
+ name: string;
3
+ url: string;
4
+ }
5
+ /** cabeçalho/rodapé da folha, editados no canvas (mesmo formato do case) */
6
+ export interface MinutaChrome {
7
+ header?: string;
8
+ footer?: string;
9
+ headerScope?: 'all' | 'first';
10
+ footerScope?: 'all' | 'first';
11
+ showPageNumber?: boolean;
12
+ }
13
+ export interface Minuta {
14
+ id: string;
15
+ name: string;
16
+ status: 'PROCESSING' | 'READY' | 'ERROR';
17
+ files: MinutaFile[];
18
+ context?: string | null;
19
+ agentId?: string | null;
20
+ analysis?: MinutaAnalysis | null;
21
+ chrome?: MinutaChrome | null;
22
+ contentHtml?: string | null;
23
+ error?: string | null;
24
+ createdByEmail?: string | null;
25
+ createdAt: string;
26
+ updatedAt: string;
27
+ }
28
+ export interface MinutaVersion {
29
+ id: string;
30
+ versionNumber: number;
31
+ versionType: 'initial' | 'auto' | 'manual';
32
+ comment?: string | null;
33
+ createdByEmail?: string | null;
34
+ createdAt: string;
35
+ }
36
+ /**
37
+ * Estado de âncora compartilhado pelas anotações: `trecho` é o recorte
38
+ * verbatim que ancora o item na minuta; quando o usuário edita esse recorte,
39
+ * a anotação vira `manual` (o trecho é atualizado pro texto editado e o
40
+ * original fica em `trechoOriginal` — se ele voltar, a anotação se restaura).
41
+ */
42
+ export interface AnnotationAnchorState {
43
+ trecho?: string;
44
+ manual?: boolean;
45
+ trechoOriginal?: string;
46
+ }
47
+ export interface MinutaAnalysis {
48
+ resumo?: string;
49
+ teses?: Array<{
50
+ titulo: string;
51
+ fundamento?: string;
52
+ onde?: string;
53
+ } & AnnotationAnchorState>;
54
+ jurisprudencias?: Array<{
55
+ referencia: string;
56
+ orgao?: string;
57
+ tese?: string;
58
+ onde?: string;
59
+ fonte?: string;
60
+ } & AnnotationAnchorState>;
61
+ citacoes?: Array<{
62
+ tipo?: string;
63
+ referencia: string;
64
+ onde?: string;
65
+ como?: string;
66
+ } & AnnotationAnchorState>;
67
+ documentos?: Array<{
68
+ arquivo: string;
69
+ uso?: string;
70
+ partes?: string[];
71
+ }>;
72
+ fatos?: Array<{
73
+ dado: string;
74
+ documento?: string;
75
+ pagina?: number;
76
+ onde?: string;
77
+ } & AnnotationAnchorState>;
78
+ /** trechos verbatim de texto ADICIONADO pelo usuário (highlight cinza persistente) */
79
+ edicoesManuais?: string[];
80
+ }
81
+ /**
82
+ * Cliente das minutas avulsas via o proxy Nitro do módulo (`/api/minuta/*`).
83
+ * A autenticação acontece no proxy (JWT do usuário do host) — nada de
84
+ * segredos no browser.
85
+ */
86
+ export declare function useMinutaApi(): {
87
+ fetchMinutas: () => Promise<Minuta[]>;
88
+ fetchMinutaById: (id: string) => Promise<Minuta>;
89
+ createMinuta: (input: {
90
+ name: string;
91
+ files: MinutaFile[];
92
+ context?: string;
93
+ agentId?: string;
94
+ }) => Promise<Minuta>;
95
+ updateMinuta: (id: string, patch: {
96
+ name?: string;
97
+ contentHtml?: string;
98
+ analysis?: MinutaAnalysis;
99
+ chrome?: MinutaChrome;
100
+ }) => Promise<Minuta>;
101
+ deleteMinuta: (id: string) => Promise<{
102
+ id: string;
103
+ }>;
104
+ uploadMinutaFile: (file: File) => Promise<MinutaFile>;
105
+ getDownloadUrl: (vaultUrl: string) => Promise<string>;
106
+ fetchVersions: (id: string) => Promise<MinutaVersion[]>;
107
+ fetchVersion: (id: string, versionNumber: number) => Promise<MinutaVersion & {
108
+ contentHtml: string;
109
+ }>;
110
+ createVersion: (id: string, comment?: string) => Promise<MinutaVersion>;
111
+ restoreVersion: (id: string, versionNumber: number) => Promise<Minuta>;
112
+ };
@@ -0,0 +1,68 @@
1
+ import { $fetch } from "ofetch";
2
+ export function useMinutaApi() {
3
+ const base = "/api/minuta";
4
+ function fetchMinutas() {
5
+ return $fetch(base);
6
+ }
7
+ function fetchMinutaById(id) {
8
+ return $fetch(`${base}/${encodeURIComponent(id)}`);
9
+ }
10
+ function createMinuta(input) {
11
+ return $fetch(base, { method: "POST", body: input });
12
+ }
13
+ function updateMinuta(id, patch) {
14
+ return $fetch(`${base}/${encodeURIComponent(id)}`, { method: "PATCH", body: patch });
15
+ }
16
+ function deleteMinuta(id) {
17
+ return $fetch(`${base}/${encodeURIComponent(id)}`, { method: "DELETE" });
18
+ }
19
+ async function uploadMinutaFile(file) {
20
+ const { uploadUrl, vaultUrl } = await $fetch(
21
+ `${base}/upload-url`,
22
+ { method: "POST", body: { fileName: file.name } }
23
+ );
24
+ const response = await fetch(uploadUrl, {
25
+ method: "PUT",
26
+ body: file,
27
+ headers: { "Content-Type": file.type || "application/octet-stream" }
28
+ });
29
+ if (!response.ok)
30
+ throw new Error(`Falha no upload de ${file.name}`);
31
+ return { name: file.name, url: vaultUrl };
32
+ }
33
+ async function getDownloadUrl(vaultUrl) {
34
+ const { downloadUrl } = await $fetch(`${base}/download-url`, {
35
+ method: "POST",
36
+ body: { vaultUrl }
37
+ });
38
+ return downloadUrl;
39
+ }
40
+ function fetchVersions(id) {
41
+ return $fetch(`${base}/${encodeURIComponent(id)}/versions`);
42
+ }
43
+ function fetchVersion(id, versionNumber) {
44
+ return $fetch(`${base}/${encodeURIComponent(id)}/versions/${versionNumber}`);
45
+ }
46
+ function createVersion(id, comment) {
47
+ return $fetch(`${base}/${encodeURIComponent(id)}/versions`, {
48
+ method: "POST",
49
+ body: comment?.trim() ? { comment: comment.trim() } : {}
50
+ });
51
+ }
52
+ function restoreVersion(id, versionNumber) {
53
+ return $fetch(`${base}/${encodeURIComponent(id)}/versions/${versionNumber}/restore`, { method: "POST" });
54
+ }
55
+ return {
56
+ fetchMinutas,
57
+ fetchMinutaById,
58
+ createMinuta,
59
+ updateMinuta,
60
+ deleteMinuta,
61
+ uploadMinutaFile,
62
+ getDownloadUrl,
63
+ fetchVersions,
64
+ fetchVersion,
65
+ createVersion,
66
+ restoreVersion
67
+ };
68
+ }
@@ -0,0 +1,12 @@
1
+ import type { Editor } from '@tiptap/core';
2
+ /** mesmo escape do lazy-vault-images pro valor do atributo */
3
+ export declare function escapeVaultAttr(value: string): string;
4
+ export declare function decodeVaultAttr(value: string): string;
5
+ /** tag de imagem que o editor insere após o upload */
6
+ export declare function vaultImageHtml(src: string, vaultUrl: string): string;
7
+ /**
8
+ * Re-resolve todas as imagens vault:// do documento (o src presignado salvo
9
+ * pode ter expirado). Transação fora do histórico de undo e marcada como
10
+ * programática (não vira "texto adicionado" no rastro manual).
11
+ */
12
+ export declare function refreshVaultImages(editor: Editor, resolveUrl: (vaultUrl: string) => Promise<string>): Promise<number>;
@@ -0,0 +1,85 @@
1
+ import { PROGRAMMATIC_META } from "./annotationAnchors.js";
2
+ function escapeAttr(value) {
3
+ return value.replace(/&/g, "&amp;").replace(/"/g, "&quot;");
4
+ }
5
+ function escapeRegExp(value) {
6
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
7
+ }
8
+ export function escapeVaultAttr(value) {
9
+ return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
10
+ }
11
+ export function decodeVaultAttr(value) {
12
+ return value.replace(/&quot;/g, '"').replace(/&gt;/g, ">").replace(/&lt;/g, "<").replace(/&amp;/g, "&");
13
+ }
14
+ export function vaultImageHtml(src, vaultUrl) {
15
+ return `<img src="${escapeAttr(src)}" data-vault-src="${escapeVaultAttr(vaultUrl)}" style="width: 420px; height: auto; max-width: 100%;">`;
16
+ }
17
+ function swapSrcKeepRef(html, attrValue, freshUrl) {
18
+ const needle = `data-vault-src="${attrValue}"`;
19
+ if (!html.includes(needle))
20
+ return html;
21
+ const tagRe = new RegExp(`<img\\b[^>]*?${escapeRegExp(needle)}[^>]*?>`, "g");
22
+ return html.replace(tagRe, (tag) => tag.replace(/\ssrc="[^"]*"/, ` src="${escapeAttr(freshUrl)}"`));
23
+ }
24
+ export async function refreshVaultImages(editor, resolveUrl) {
25
+ if (!editor || editor.isDestroyed)
26
+ return 0;
27
+ const targets = [];
28
+ editor.state.doc.descendants((node, pos) => {
29
+ if (node.type.name !== "htmlIsland")
30
+ return;
31
+ const html = String(node.attrs.html ?? "");
32
+ const refs = [...html.matchAll(/data-vault-src="([^"]+)"/g)].map((m) => m[1]);
33
+ if (refs.length)
34
+ targets.push({ pos, html, refs });
35
+ });
36
+ if (!targets.length)
37
+ return 0;
38
+ const unique = [...new Set(targets.flatMap((t) => t.refs))];
39
+ const resolved = /* @__PURE__ */ new Map();
40
+ await Promise.all(unique.map(async (attrValue) => {
41
+ try {
42
+ const fresh = await resolveUrl(decodeVaultAttr(attrValue));
43
+ if (!fresh)
44
+ return;
45
+ await new Promise((done) => {
46
+ const img = new Image();
47
+ const finish = () => done();
48
+ img.onload = finish;
49
+ img.onerror = finish;
50
+ setTimeout(finish, 4e3);
51
+ img.src = fresh;
52
+ });
53
+ resolved.set(attrValue, fresh);
54
+ } catch (error) {
55
+ console.error("minuta: falha ao resolver imagem do vault", attrValue, error);
56
+ }
57
+ }));
58
+ if (!resolved.size || editor.isDestroyed)
59
+ return 0;
60
+ const tr = editor.state.tr;
61
+ let touched = 0;
62
+ editor.state.doc.descendants((node, pos) => {
63
+ if (node.type.name !== "htmlIsland")
64
+ return;
65
+ let html = String(node.attrs.html ?? "");
66
+ let changed = false;
67
+ for (const [attrValue, freshUrl] of resolved) {
68
+ const next = swapSrcKeepRef(html, attrValue, freshUrl);
69
+ if (next !== html) {
70
+ html = next;
71
+ changed = true;
72
+ }
73
+ }
74
+ if (changed) {
75
+ tr.setNodeMarkup(pos, void 0, { ...node.attrs, html });
76
+ touched++;
77
+ }
78
+ });
79
+ if (!touched)
80
+ return 0;
81
+ tr.setMeta("addToHistory", false);
82
+ tr.setMeta(PROGRAMMATIC_META, true);
83
+ editor.view.dispatch(tr);
84
+ return touched;
85
+ }