@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.
- package/README.md +106 -0
- package/dist/module.cjs +5 -0
- package/dist/module.d.mts +18 -0
- package/dist/module.d.ts +18 -0
- package/dist/module.json +9 -0
- package/dist/module.mjs +45 -0
- package/dist/runtime/components/MinutaAnnotationsGutter.vue +425 -0
- package/dist/runtime/components/MinutaChatEditCard.vue +124 -0
- package/dist/runtime/components/MinutaChromeCanvas.vue +549 -0
- package/dist/runtime/components/MinutaChromeEditor.vue +100 -0
- package/dist/runtime/components/MinutaDocumentViewer.vue +90 -0
- package/dist/runtime/components/MinutaDocxEditor.vue +1158 -0
- package/dist/runtime/components/MinutaEmbed.vue +783 -0
- package/dist/runtime/components/MinutaIcon.vue +53 -0
- package/dist/runtime/composables/annotationAnchors.d.ts +73 -0
- package/dist/runtime/composables/annotationAnchors.js +243 -0
- package/dist/runtime/composables/chatEditOps.d.ts +37 -0
- package/dist/runtime/composables/chatEditOps.js +61 -0
- package/dist/runtime/composables/docxExport.d.ts +24 -0
- package/dist/runtime/composables/docxExport.js +1292 -0
- package/dist/runtime/composables/editorExtensions.d.ts +56 -0
- package/dist/runtime/composables/editorExtensions.js +466 -0
- package/dist/runtime/composables/paginationPlugin.d.ts +126 -0
- package/dist/runtime/composables/paginationPlugin.js +421 -0
- package/dist/runtime/composables/useMinutaApi.d.ts +112 -0
- package/dist/runtime/composables/useMinutaApi.js +68 -0
- package/dist/runtime/composables/vaultImages.d.ts +12 -0
- package/dist/runtime/composables/vaultImages.js +85 -0
- package/dist/runtime/composables/versionDiff.d.ts +34 -0
- package/dist/runtime/composables/versionDiff.js +62 -0
- package/dist/runtime/server/api/minuta/[...path].d.ts +2 -0
- package/dist/runtime/server/api/minuta/[...path].js +6 -0
- package/dist/runtime/server/api/minuta/index.d.ts +2 -0
- package/dist/runtime/server/api/minuta/index.js +3 -0
- package/dist/runtime/server/utils/minuta-proxy.d.ts +10 -0
- package/dist/runtime/server/utils/minuta-proxy.js +63 -0
- package/dist/runtime/utils/footnoteHtml.d.ts +26 -0
- package/dist/runtime/utils/footnoteHtml.js +117 -0
- package/dist/runtime/utils/pocKeepWithNext.d.ts +44 -0
- package/dist/runtime/utils/pocKeepWithNext.js +9 -0
- package/dist/runtime/utils/sanitizeRichHtml.d.ts +10 -0
- package/dist/runtime/utils/sanitizeRichHtml.js +73 -0
- package/dist/types.d.mts +7 -0
- package/dist/types.d.ts +7 -0
- package/package.json +100 -0
|
@@ -0,0 +1,783 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { Editor } from '@tiptap/core'
|
|
3
|
+
import { computed, markRaw, nextTick, onMounted, onUnmounted, reactive, ref, resolveComponent, shallowRef, watch } from 'vue'
|
|
4
|
+
import type { Minuta, MinutaAnalysis, MinutaChrome, MinutaVersion } from '../composables/useMinutaApi'
|
|
5
|
+
import { useMinutaApi } from '../composables/useMinutaApi'
|
|
6
|
+
import type { ApplyChatEditResult, MinutaChatEdit } from '../composables/chatEditOps'
|
|
7
|
+
import { applyChatEditOps, mergeChatAnnotations } from '../composables/chatEditOps'
|
|
8
|
+
import { pocPageKey } from '../composables/paginationPlugin'
|
|
9
|
+
import { pocEnterVersionDiff, pocExitVersionDiff } from '../composables/versionDiff'
|
|
10
|
+
import { refreshVaultImages } from '../composables/vaultImages'
|
|
11
|
+
import MinutaDocxEditor from './MinutaDocxEditor.vue'
|
|
12
|
+
import MinutaAnnotationsGutter from './MinutaAnnotationsGutter.vue'
|
|
13
|
+
import MinutaChatEditCard from './MinutaChatEditCard.vue'
|
|
14
|
+
import MinutaChromeEditor from './MinutaChromeEditor.vue'
|
|
15
|
+
import MinutaDocumentViewer from './MinutaDocumentViewer.vue'
|
|
16
|
+
|
|
17
|
+
const props = withDefaults(defineProps<{
|
|
18
|
+
minutaId: string
|
|
19
|
+
/** permite editar o documento (default: true) */
|
|
20
|
+
editable?: boolean
|
|
21
|
+
/** salva automaticamente 2s após a última edição (default: true) */
|
|
22
|
+
autoSave?: boolean
|
|
23
|
+
/** mostra a barra superior com nome, status de save e export (default: true) */
|
|
24
|
+
showToolbar?: boolean
|
|
25
|
+
/** margens da página em px @96dpi (default: 2,5cm topo/rodapé, 3cm laterais) */
|
|
26
|
+
margins?: { top: number, right: number, bottom: number, left: number }
|
|
27
|
+
/**
|
|
28
|
+
* id de plataforma de um agente Tela de edição de minuta: habilita o
|
|
29
|
+
* assistente de chat na coluna esquerda. Requer `@meistrari/chat-nuxt`
|
|
30
|
+
* registrado como módulo no app host (o pacote já traz a dependência).
|
|
31
|
+
*/
|
|
32
|
+
chatAgentId?: string | null
|
|
33
|
+
/** deixa o nome da minuta editável na barra (default: segue `editable`) */
|
|
34
|
+
renamable?: boolean
|
|
35
|
+
}>(), {
|
|
36
|
+
editable: true,
|
|
37
|
+
autoSave: true,
|
|
38
|
+
showToolbar: true,
|
|
39
|
+
margins: () => ({ top: 95, right: 113, bottom: 95, left: 113 }),
|
|
40
|
+
chatAgentId: null,
|
|
41
|
+
renamable: undefined,
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
const emit = defineEmits<{
|
|
45
|
+
ready: [editor: Editor]
|
|
46
|
+
saved: [minuta: Minuta]
|
|
47
|
+
error: [message: string]
|
|
48
|
+
}>()
|
|
49
|
+
|
|
50
|
+
const HEADER = { logoUrl: null as string | null, position: 'left' as const }
|
|
51
|
+
|
|
52
|
+
// cabeçalho/rodapé editáveis no canvas (persistidos na minuta)
|
|
53
|
+
const chromeText = reactive({
|
|
54
|
+
header: '',
|
|
55
|
+
footer: '',
|
|
56
|
+
headerScope: 'all' as 'all' | 'first',
|
|
57
|
+
footerScope: 'all' as 'all' | 'first',
|
|
58
|
+
showPageNumber: true,
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
function hydrateChrome(chrome?: MinutaChrome | null) {
|
|
62
|
+
chromeText.header = chrome?.header ?? ''
|
|
63
|
+
chromeText.footer = chrome?.footer ?? ''
|
|
64
|
+
chromeText.headerScope = chrome?.headerScope ?? 'all'
|
|
65
|
+
chromeText.footerScope = chrome?.footerScope ?? 'all'
|
|
66
|
+
chromeText.showPageNumber = chrome?.showPageNumber !== false
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const api = useMinutaApi()
|
|
70
|
+
|
|
71
|
+
const minuta = ref<Minuta | null>(null)
|
|
72
|
+
const isLoading = ref(true)
|
|
73
|
+
const loadError = ref<string | null>(null)
|
|
74
|
+
const editor = shallowRef<Editor | null>(null)
|
|
75
|
+
// preview de uma versão antiga: trava auto-save, export e edição
|
|
76
|
+
const versionPreview = ref<{ versionNumber: number, createdAt: string } | null>(null)
|
|
77
|
+
// debounce do PATCH de análise (anotações mudam a cada edição do texto)
|
|
78
|
+
let analysisPatchTimer: ReturnType<typeof setTimeout> | null = null
|
|
79
|
+
const saveState = ref<'idle' | 'pending' | 'saving' | 'saved' | 'error'>('idle')
|
|
80
|
+
const isExporting = ref(false)
|
|
81
|
+
|
|
82
|
+
// nome editável na barra: o host que renderiza sem toolbar própria (ou que só
|
|
83
|
+
// injeta um botão de voltar) continua com o rename dentro do embed
|
|
84
|
+
const canRename = computed(() => props.renamable ?? props.editable)
|
|
85
|
+
const editableName = ref('')
|
|
86
|
+
const nameInputRef = ref<HTMLInputElement | null>(null)
|
|
87
|
+
// não sobrescreve o que o usuário está digitando quando o poll traz a minuta
|
|
88
|
+
watch(minuta, (m) => {
|
|
89
|
+
if (m && (!import.meta.client || document.activeElement !== nameInputRef.value))
|
|
90
|
+
editableName.value = m.name
|
|
91
|
+
}, { immediate: true })
|
|
92
|
+
|
|
93
|
+
async function saveName() {
|
|
94
|
+
const name = editableName.value.trim()
|
|
95
|
+
if (!minuta.value || !name || name === minuta.value.name) {
|
|
96
|
+
editableName.value = minuta.value?.name ?? ''
|
|
97
|
+
return
|
|
98
|
+
}
|
|
99
|
+
try {
|
|
100
|
+
minuta.value = await api.updateMinuta(props.minutaId, { name })
|
|
101
|
+
}
|
|
102
|
+
catch (err) {
|
|
103
|
+
editableName.value = minuta.value.name
|
|
104
|
+
emit('error', err instanceof Error ? err.message : 'Falha ao renomear a minuta')
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
let pollTimer: ReturnType<typeof setTimeout> | null = null
|
|
109
|
+
let saveTimer: ReturnType<typeof setTimeout> | null = null
|
|
110
|
+
|
|
111
|
+
async function loadMinuta() {
|
|
112
|
+
try {
|
|
113
|
+
const fetched = await api.fetchMinutaById(props.minutaId)
|
|
114
|
+
minuta.value = fetched
|
|
115
|
+
hydrateChrome(fetched.chrome)
|
|
116
|
+
if (fetched.status === 'PROCESSING')
|
|
117
|
+
schedulePoll()
|
|
118
|
+
}
|
|
119
|
+
catch (err) {
|
|
120
|
+
loadError.value = err instanceof Error ? err.message : 'Falha ao carregar a minuta'
|
|
121
|
+
emit('error', loadError.value)
|
|
122
|
+
}
|
|
123
|
+
finally {
|
|
124
|
+
isLoading.value = false
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function schedulePoll() {
|
|
129
|
+
pollTimer = setTimeout(async () => {
|
|
130
|
+
try {
|
|
131
|
+
const fetched = await api.fetchMinutaById(props.minutaId)
|
|
132
|
+
minuta.value = fetched
|
|
133
|
+
if (fetched.status === 'PROCESSING')
|
|
134
|
+
schedulePoll()
|
|
135
|
+
}
|
|
136
|
+
catch {
|
|
137
|
+
schedulePoll()
|
|
138
|
+
}
|
|
139
|
+
}, 3000)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// só no cliente: `/api/minuta` é rota do próprio app (URL relativa), que não
|
|
143
|
+
// resolve durante o SSR de um host que renderize no servidor
|
|
144
|
+
onMounted(loadMinuta)
|
|
145
|
+
|
|
146
|
+
onUnmounted(() => {
|
|
147
|
+
if (pollTimer)
|
|
148
|
+
clearTimeout(pollTimer)
|
|
149
|
+
if (saveTimer)
|
|
150
|
+
clearTimeout(saveTimer)
|
|
151
|
+
if (analysisPatchTimer)
|
|
152
|
+
clearTimeout(analysisPatchTimer)
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
// --- imagens do corpo no Vault: sobe no insert, re-resolve o src no load ---
|
|
156
|
+
async function uploadEditorImage(file: File) {
|
|
157
|
+
const uploaded = await api.uploadMinutaFile(file)
|
|
158
|
+
if (!uploaded?.url)
|
|
159
|
+
return null
|
|
160
|
+
const src = await api.getDownloadUrl(uploaded.url)
|
|
161
|
+
return { src, vaultSrc: uploaded.url }
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function refreshEditorVaultImages() {
|
|
165
|
+
const ed = editor.value
|
|
166
|
+
if (!ed || ed.isDestroyed)
|
|
167
|
+
return
|
|
168
|
+
void refreshVaultImages(ed, url => api.getDownloadUrl(url))
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function onEditorReady(ed: Editor) {
|
|
172
|
+
editor.value = ed
|
|
173
|
+
emit('ready', ed)
|
|
174
|
+
void nextTick(refreshEditorVaultImages)
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function onEditorChanged() {
|
|
178
|
+
if (!props.autoSave || versionPreview.value)
|
|
179
|
+
return
|
|
180
|
+
saveState.value = 'pending'
|
|
181
|
+
if (saveTimer)
|
|
182
|
+
clearTimeout(saveTimer)
|
|
183
|
+
saveTimer = setTimeout(() => void saveNow(), 2000)
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
async function saveNow(): Promise<boolean> {
|
|
187
|
+
const ed = editor.value
|
|
188
|
+
if (!ed || ed.isDestroyed || !minuta.value)
|
|
189
|
+
return false
|
|
190
|
+
if (versionPreview.value)
|
|
191
|
+
return true
|
|
192
|
+
if (saveTimer) {
|
|
193
|
+
clearTimeout(saveTimer)
|
|
194
|
+
saveTimer = null
|
|
195
|
+
}
|
|
196
|
+
saveState.value = 'saving'
|
|
197
|
+
try {
|
|
198
|
+
const updated = await api.updateMinuta(props.minutaId, { contentHtml: ed.getHTML() })
|
|
199
|
+
minuta.value = updated
|
|
200
|
+
saveState.value = 'saved'
|
|
201
|
+
emit('saved', updated)
|
|
202
|
+
return true
|
|
203
|
+
}
|
|
204
|
+
catch (err) {
|
|
205
|
+
console.error('[minuta-nuxt] erro ao salvar:', err)
|
|
206
|
+
saveState.value = 'error'
|
|
207
|
+
emit('error', 'Falha ao salvar a minuta')
|
|
208
|
+
return false
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
async function exportDocx() {
|
|
213
|
+
const ed = editor.value
|
|
214
|
+
if (!ed || ed.isDestroyed || isExporting.value || versionPreview.value)
|
|
215
|
+
return
|
|
216
|
+
isExporting.value = true
|
|
217
|
+
try {
|
|
218
|
+
await saveNow()
|
|
219
|
+
// a lib `docx` é o maior pedaço do bundle e só serve no clique:
|
|
220
|
+
// carregar sob demanda tira ~1 MB do abrir da minuta
|
|
221
|
+
const { pocExportDocx } = await import('../composables/docxExport')
|
|
222
|
+
const breaks = pocPageKey.getState(ed.state)?.breaks ?? []
|
|
223
|
+
const blob = await pocExportDocx(ed, props.margins, {
|
|
224
|
+
headerText: chromeText.header,
|
|
225
|
+
footerText: chromeText.footer,
|
|
226
|
+
headerScope: chromeText.headerScope,
|
|
227
|
+
footerScope: chromeText.footerScope,
|
|
228
|
+
headerLogoUrl: HEADER.logoUrl,
|
|
229
|
+
headerPosition: HEADER.position,
|
|
230
|
+
showPageNumber: chromeText.showPageNumber,
|
|
231
|
+
}, { breaks })
|
|
232
|
+
|
|
233
|
+
const url = URL.createObjectURL(blob)
|
|
234
|
+
const link = document.createElement('a')
|
|
235
|
+
link.href = url
|
|
236
|
+
link.download = `${(minuta.value?.name || 'minuta').trim()}.docx`
|
|
237
|
+
link.click()
|
|
238
|
+
URL.revokeObjectURL(url)
|
|
239
|
+
}
|
|
240
|
+
catch (err) {
|
|
241
|
+
console.error('[minuta-nuxt] erro ao exportar:', err)
|
|
242
|
+
emit('error', 'Falha ao exportar o DOCX')
|
|
243
|
+
}
|
|
244
|
+
finally {
|
|
245
|
+
isExporting.value = false
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// ==========================================
|
|
250
|
+
// Histórico (preview + diff + restore — mesmo fluxo do defesa.ai)
|
|
251
|
+
// ==========================================
|
|
252
|
+
|
|
253
|
+
const historyOpen = ref(false)
|
|
254
|
+
const versions = ref<MinutaVersion[]>([])
|
|
255
|
+
const loadingVersions = ref(false)
|
|
256
|
+
const savingCheckpoint = ref(false)
|
|
257
|
+
const restoring = ref(false)
|
|
258
|
+
const previewLoading = ref(false)
|
|
259
|
+
// preview/volta de versão trocam o doc inteiro — re-resolve os src das imagens
|
|
260
|
+
watch(versionPreview, () => void nextTick(refreshEditorVaultImages))
|
|
261
|
+
const compareMode = ref(false)
|
|
262
|
+
const compareChangeCount = ref(0)
|
|
263
|
+
const checkpointComment = ref('')
|
|
264
|
+
let baselineHtml: string | null = null
|
|
265
|
+
|
|
266
|
+
const VERSION_TYPE_LABEL: Record<string, string> = {
|
|
267
|
+
initial: 'Original',
|
|
268
|
+
auto: 'Automática',
|
|
269
|
+
manual: 'Manual',
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function formatVersionDate(iso: string): string {
|
|
273
|
+
try {
|
|
274
|
+
return new Date(iso).toLocaleString('pt-BR')
|
|
275
|
+
}
|
|
276
|
+
catch {
|
|
277
|
+
return iso
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
async function loadVersions() {
|
|
282
|
+
loadingVersions.value = true
|
|
283
|
+
try {
|
|
284
|
+
versions.value = await api.fetchVersions(props.minutaId)
|
|
285
|
+
}
|
|
286
|
+
catch {
|
|
287
|
+
emit('error', 'Falha ao carregar o histórico')
|
|
288
|
+
}
|
|
289
|
+
finally {
|
|
290
|
+
loadingVersions.value = false
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function toggleHistory() {
|
|
295
|
+
if (historyOpen.value) {
|
|
296
|
+
closeHistory()
|
|
297
|
+
return
|
|
298
|
+
}
|
|
299
|
+
historyOpen.value = true
|
|
300
|
+
void loadVersions()
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function closeHistory() {
|
|
304
|
+
historyOpen.value = false
|
|
305
|
+
exitPreview()
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
async function saveCheckpoint() {
|
|
309
|
+
if (savingCheckpoint.value)
|
|
310
|
+
return
|
|
311
|
+
savingCheckpoint.value = true
|
|
312
|
+
try {
|
|
313
|
+
if (!await saveNow())
|
|
314
|
+
return
|
|
315
|
+
await api.createVersion(props.minutaId, checkpointComment.value)
|
|
316
|
+
checkpointComment.value = ''
|
|
317
|
+
await loadVersions()
|
|
318
|
+
}
|
|
319
|
+
catch {
|
|
320
|
+
emit('error', 'Falha ao salvar a versão')
|
|
321
|
+
}
|
|
322
|
+
finally {
|
|
323
|
+
savingCheckpoint.value = false
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
async function enterPreview(versionNumber: number) {
|
|
328
|
+
const ed = editor.value
|
|
329
|
+
if (!ed || ed.isDestroyed || previewLoading.value)
|
|
330
|
+
return
|
|
331
|
+
if (versionPreview.value?.versionNumber === versionNumber)
|
|
332
|
+
return
|
|
333
|
+
previewLoading.value = true
|
|
334
|
+
try {
|
|
335
|
+
if (!versionPreview.value && !await saveNow())
|
|
336
|
+
return
|
|
337
|
+
if (compareMode.value) {
|
|
338
|
+
pocExitVersionDiff(ed)
|
|
339
|
+
compareMode.value = false
|
|
340
|
+
}
|
|
341
|
+
const full = await api.fetchVersion(props.minutaId, versionNumber)
|
|
342
|
+
// guard entra ANTES do setContent — o @changed da troca não salva
|
|
343
|
+
versionPreview.value = { versionNumber: full.versionNumber, createdAt: formatVersionDate(full.createdAt) }
|
|
344
|
+
ed.commands.setContent(full.contentHtml)
|
|
345
|
+
}
|
|
346
|
+
catch {
|
|
347
|
+
emit('error', 'Falha ao abrir esta versão')
|
|
348
|
+
}
|
|
349
|
+
finally {
|
|
350
|
+
previewLoading.value = false
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
function exitPreview() {
|
|
355
|
+
const ed = editor.value
|
|
356
|
+
if (!versionPreview.value)
|
|
357
|
+
return
|
|
358
|
+
if (ed && !ed.isDestroyed) {
|
|
359
|
+
pocExitVersionDiff(ed)
|
|
360
|
+
if (minuta.value?.contentHtml != null)
|
|
361
|
+
ed.commands.setContent(minuta.value.contentHtml)
|
|
362
|
+
}
|
|
363
|
+
compareMode.value = false
|
|
364
|
+
versionPreview.value = null
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/** diff da versão em preview contra a v1 (o documento como o agente entregou) */
|
|
368
|
+
async function toggleCompare() {
|
|
369
|
+
const ed = editor.value
|
|
370
|
+
if (!versionPreview.value || !ed || ed.isDestroyed)
|
|
371
|
+
return
|
|
372
|
+
if (compareMode.value) {
|
|
373
|
+
pocExitVersionDiff(ed)
|
|
374
|
+
compareMode.value = false
|
|
375
|
+
return
|
|
376
|
+
}
|
|
377
|
+
if (baselineHtml === null) {
|
|
378
|
+
try {
|
|
379
|
+
baselineHtml = (await api.fetchVersion(props.minutaId, 1)).contentHtml
|
|
380
|
+
}
|
|
381
|
+
catch {
|
|
382
|
+
emit('error', 'Falha ao carregar a versão original para comparar')
|
|
383
|
+
return
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
const { changeCount } = pocEnterVersionDiff(ed, baselineHtml)
|
|
387
|
+
compareChangeCount.value = changeCount
|
|
388
|
+
compareMode.value = true
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
async function restoreVersion(versionNumber: number) {
|
|
392
|
+
const ed = editor.value
|
|
393
|
+
if (restoring.value)
|
|
394
|
+
return
|
|
395
|
+
restoring.value = true
|
|
396
|
+
try {
|
|
397
|
+
const restored = await api.restoreVersion(props.minutaId, versionNumber)
|
|
398
|
+
minuta.value = restored
|
|
399
|
+
if (ed && !ed.isDestroyed) {
|
|
400
|
+
pocExitVersionDiff(ed)
|
|
401
|
+
ed.commands.setContent(restored.contentHtml || '<p></p>')
|
|
402
|
+
}
|
|
403
|
+
compareMode.value = false
|
|
404
|
+
versionPreview.value = null
|
|
405
|
+
saveState.value = 'saved'
|
|
406
|
+
await loadVersions()
|
|
407
|
+
void nextTick(refreshEditorVaultImages)
|
|
408
|
+
}
|
|
409
|
+
catch {
|
|
410
|
+
emit('error', 'Falha ao restaurar a versão')
|
|
411
|
+
}
|
|
412
|
+
finally {
|
|
413
|
+
restoring.value = false
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// ==========================================
|
|
418
|
+
// Edição de cabeçalho/rodapé (canvas estilo Word, salva na minuta)
|
|
419
|
+
// ==========================================
|
|
420
|
+
|
|
421
|
+
const chromeEditor = ref<{ part: 'header' | 'footer' } | null>(null)
|
|
422
|
+
|
|
423
|
+
function onEditChrome(event: Event) {
|
|
424
|
+
const { part } = (event as CustomEvent).detail || {}
|
|
425
|
+
if (part !== 'header' && part !== 'footer')
|
|
426
|
+
return
|
|
427
|
+
if (versionPreview.value)
|
|
428
|
+
return
|
|
429
|
+
chromeEditor.value = { part }
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
async function applyChrome({ html, scope, showPageNumber }: { html: string, scope: 'all' | 'first', showPageNumber?: boolean }) {
|
|
433
|
+
if (!chromeEditor.value)
|
|
434
|
+
return
|
|
435
|
+
const part = chromeEditor.value.part
|
|
436
|
+
chromeText[part] = html
|
|
437
|
+
chromeText[part === 'header' ? 'headerScope' : 'footerScope'] = scope
|
|
438
|
+
if (showPageNumber !== undefined)
|
|
439
|
+
chromeText.showPageNumber = showPageNumber
|
|
440
|
+
chromeEditor.value = null
|
|
441
|
+
try {
|
|
442
|
+
minuta.value = await api.updateMinuta(props.minutaId, { chrome: { ...chromeText } })
|
|
443
|
+
}
|
|
444
|
+
catch {
|
|
445
|
+
emit('error', 'Falha ao salvar o cabeçalho/rodapé')
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
onMounted(() => window.addEventListener('poc:edit-chrome', onEditChrome))
|
|
450
|
+
onUnmounted(() => window.removeEventListener('poc:edit-chrome', onEditChrome))
|
|
451
|
+
|
|
452
|
+
// ==========================================
|
|
453
|
+
// Assistente de chat (agente Tela edita a peça via blocos minuta-edit)
|
|
454
|
+
// ==========================================
|
|
455
|
+
|
|
456
|
+
/** componente do chat-nuxt — presente só quando o host registra o módulo */
|
|
457
|
+
const resolvedChatEmbed = resolveComponent('MeistrariChatEmbed')
|
|
458
|
+
const chatEmbedComponent = typeof resolvedChatEmbed === 'string' ? null : markRaw(resolvedChatEmbed as object)
|
|
459
|
+
const chatComponents = { 'minuta-edit': markRaw(MinutaChatEditCard) }
|
|
460
|
+
|
|
461
|
+
function htmlToPlainText(html: string): string {
|
|
462
|
+
const el = document.createElement('div')
|
|
463
|
+
el.innerHTML = html
|
|
464
|
+
return el.textContent ?? ''
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
/** inputs reenviados a cada mensagem: documento atual + análise + nome */
|
|
468
|
+
const chatInputs = computed(() => {
|
|
469
|
+
const current = minuta.value
|
|
470
|
+
if (!current)
|
|
471
|
+
return []
|
|
472
|
+
// contentHtml muda a cada save → recomputa com o texto vivo do editor
|
|
473
|
+
void current.contentHtml
|
|
474
|
+
const documento = editor.value && !editor.value.isDestroyed
|
|
475
|
+
? editor.value.getText({ blockSeparator: '\n\n' })
|
|
476
|
+
: htmlToPlainText(current.contentHtml || '')
|
|
477
|
+
return [
|
|
478
|
+
{ type: 'text' as const, name: 'documento', content: documento },
|
|
479
|
+
{ type: 'text' as const, name: 'analise', content: JSON.stringify(current.analysis ?? {}) },
|
|
480
|
+
{ type: 'text' as const, name: 'minuta_nome', content: current.name },
|
|
481
|
+
]
|
|
482
|
+
})
|
|
483
|
+
|
|
484
|
+
/** card "Aplicar" do chat → ops no editor + anotações novas na análise */
|
|
485
|
+
async function onChatEditEvent(event: Event) {
|
|
486
|
+
const detail = (event as CustomEvent).detail as { edit: MinutaChatEdit, onResult?: (result: ApplyChatEditResult) => void } | undefined
|
|
487
|
+
if (!detail?.edit)
|
|
488
|
+
return
|
|
489
|
+
const ed = editor.value
|
|
490
|
+
if (!ed || ed.isDestroyed || !minuta.value) {
|
|
491
|
+
detail.onResult?.({ applied: 0, failures: ['Editor indisponível'] })
|
|
492
|
+
return
|
|
493
|
+
}
|
|
494
|
+
if (versionPreview.value) {
|
|
495
|
+
detail.onResult?.({ applied: 0, failures: ['Volte para a versão atual antes de aplicar edições'] })
|
|
496
|
+
return
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
const result = applyChatEditOps(ed, detail.edit.ops ?? [])
|
|
500
|
+
if (result.applied > 0)
|
|
501
|
+
await saveNow()
|
|
502
|
+
|
|
503
|
+
if (detail.edit.anotacoes?.length) {
|
|
504
|
+
try {
|
|
505
|
+
const merged = mergeChatAnnotations(minuta.value.analysis, detail.edit.anotacoes)
|
|
506
|
+
minuta.value = await api.updateMinuta(props.minutaId, { analysis: merged })
|
|
507
|
+
}
|
|
508
|
+
catch {
|
|
509
|
+
result.failures.push('Falha ao salvar as anotações novas')
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
detail.onResult?.(result)
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
onMounted(() => window.addEventListener('minuta-nuxt:chat-edit', onChatEditEvent))
|
|
516
|
+
onUnmounted(() => window.removeEventListener('minuta-nuxt:chat-edit', onChatEditEvent))
|
|
517
|
+
|
|
518
|
+
// ==========================================
|
|
519
|
+
// Análise da peça (comentários de margem, sempre visíveis)
|
|
520
|
+
// ==========================================
|
|
521
|
+
|
|
522
|
+
const editorCompRef = ref<any>(null)
|
|
523
|
+
const docPreviewOpen = ref(false)
|
|
524
|
+
const docPreviewUrl = ref<string | null>(null)
|
|
525
|
+
const docPreviewPage = ref<number | null>(null)
|
|
526
|
+
|
|
527
|
+
function openDocPreview(url?: string | null) {
|
|
528
|
+
if (!minuta.value?.files.length)
|
|
529
|
+
return
|
|
530
|
+
docPreviewUrl.value = url ?? minuta.value.files[0].url
|
|
531
|
+
docPreviewOpen.value = true
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
// --- persistência do estado de âncora (anotações editadas + texto adicionado) ---
|
|
535
|
+
function onAnalysisUpdate(analysis: MinutaAnalysis) {
|
|
536
|
+
// durante preview de versão o documento não é o atual — não persiste nada
|
|
537
|
+
if (versionPreview.value || !minuta.value)
|
|
538
|
+
return
|
|
539
|
+
// otimista (evita re-emissão do gutter); PATCH debounced
|
|
540
|
+
minuta.value = { ...minuta.value, analysis }
|
|
541
|
+
if (analysisPatchTimer)
|
|
542
|
+
clearTimeout(analysisPatchTimer)
|
|
543
|
+
analysisPatchTimer = setTimeout(async () => {
|
|
544
|
+
try {
|
|
545
|
+
await api.updateMinuta(props.minutaId, { analysis })
|
|
546
|
+
}
|
|
547
|
+
catch (err) {
|
|
548
|
+
console.error('[minuta-nuxt] falha ao persistir estado das anotações:', err)
|
|
549
|
+
}
|
|
550
|
+
}, 800)
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
/** card de fato extraído → abre o documento-fonte no visualizador */
|
|
554
|
+
function onOpenDocument(payload: { documento: string, pagina?: number, highlight?: string }) {
|
|
555
|
+
const file = minuta.value?.files.find(f => f.name === payload.documento)
|
|
556
|
+
?? minuta.value?.files.find(f => f.name.includes(payload.documento) || payload.documento.includes(f.name))
|
|
557
|
+
docPreviewPage.value = payload.pagina ?? null
|
|
558
|
+
openDocPreview(file?.url)
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
const hasAnalysis = computed(() => {
|
|
562
|
+
const analysis = minuta.value?.analysis
|
|
563
|
+
if (!analysis)
|
|
564
|
+
return false
|
|
565
|
+
return Boolean(analysis.resumo || analysis.teses?.length || analysis.jurisprudencias?.length
|
|
566
|
+
|| analysis.citacoes?.length || analysis.documentos?.length || analysis.fatos?.length)
|
|
567
|
+
})
|
|
568
|
+
|
|
569
|
+
const SAVE_LABEL: Record<'idle' | 'pending' | 'saving' | 'saved' | 'error', string> = {
|
|
570
|
+
idle: '',
|
|
571
|
+
pending: 'Alterações pendentes…',
|
|
572
|
+
saving: 'Salvando…',
|
|
573
|
+
saved: 'Salvo',
|
|
574
|
+
error: 'Erro ao salvar',
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
defineExpose({ saveNow, exportDocx, editor })
|
|
578
|
+
</script>
|
|
579
|
+
|
|
580
|
+
<template>
|
|
581
|
+
<div class="minuta-embed">
|
|
582
|
+
<div v-if="showToolbar && minuta" class="minuta-embed__toolbar">
|
|
583
|
+
<slot name="toolbar-start" />
|
|
584
|
+
<input
|
|
585
|
+
v-if="canRename"
|
|
586
|
+
ref="nameInputRef"
|
|
587
|
+
v-model="editableName"
|
|
588
|
+
class="minuta-embed__name minuta-embed__name--input"
|
|
589
|
+
:disabled="minuta.status !== 'READY'"
|
|
590
|
+
@blur="saveName"
|
|
591
|
+
@keydown.enter="($event.target as HTMLInputElement).blur()"
|
|
592
|
+
>
|
|
593
|
+
<span v-else class="minuta-embed__name">{{ minuta.name }}</span>
|
|
594
|
+
<span class="minuta-embed__save-state">{{ SAVE_LABEL[saveState] }}</span>
|
|
595
|
+
<button
|
|
596
|
+
v-if="minuta.status === 'READY' && minuta.files.length"
|
|
597
|
+
class="minuta-embed__ghost-btn"
|
|
598
|
+
type="button"
|
|
599
|
+
@click="openDocPreview()"
|
|
600
|
+
>
|
|
601
|
+
Documentos
|
|
602
|
+
</button>
|
|
603
|
+
<button
|
|
604
|
+
v-if="minuta.status === 'READY'"
|
|
605
|
+
class="minuta-embed__ghost-btn"
|
|
606
|
+
:class="{ 'minuta-embed__ghost-btn--on': historyOpen }"
|
|
607
|
+
type="button"
|
|
608
|
+
@click="toggleHistory"
|
|
609
|
+
>
|
|
610
|
+
Histórico
|
|
611
|
+
</button>
|
|
612
|
+
<button
|
|
613
|
+
v-if="minuta.status === 'READY'"
|
|
614
|
+
class="minuta-embed__export"
|
|
615
|
+
:disabled="isExporting || !!versionPreview"
|
|
616
|
+
type="button"
|
|
617
|
+
@click="exportDocx"
|
|
618
|
+
>
|
|
619
|
+
{{ isExporting ? 'Exportando…' : 'Baixar DOCX' }}
|
|
620
|
+
</button>
|
|
621
|
+
</div>
|
|
622
|
+
|
|
623
|
+
<div v-if="isLoading" class="minuta-embed__status">
|
|
624
|
+
Carregando…
|
|
625
|
+
</div>
|
|
626
|
+
|
|
627
|
+
<div v-else-if="loadError" class="minuta-embed__status minuta-embed__status--error">
|
|
628
|
+
{{ loadError }}
|
|
629
|
+
</div>
|
|
630
|
+
|
|
631
|
+
<div v-else-if="minuta?.status === 'PROCESSING'" class="minuta-embed__status">
|
|
632
|
+
Gerando a minuta… O agente está lendo os documentos e montando o texto.
|
|
633
|
+
</div>
|
|
634
|
+
|
|
635
|
+
<div v-else-if="minuta?.status === 'ERROR'" class="minuta-embed__status minuta-embed__status--error">
|
|
636
|
+
A geração falhou: {{ minuta.error || 'erro desconhecido' }}
|
|
637
|
+
</div>
|
|
638
|
+
|
|
639
|
+
<div v-else-if="minuta" class="minuta-embed__body">
|
|
640
|
+
<div class="minuta-embed__document">
|
|
641
|
+
<MinutaDocxEditor
|
|
642
|
+
ref="editorCompRef"
|
|
643
|
+
:initial-html="minuta.contentHtml || '<p></p>'"
|
|
644
|
+
:margins="props.margins"
|
|
645
|
+
:header="HEADER"
|
|
646
|
+
:editable="props.editable && !versionPreview"
|
|
647
|
+
:show-block-origin="false"
|
|
648
|
+
:upload-image="uploadEditorImage"
|
|
649
|
+
:chrome-text="chromeText"
|
|
650
|
+
@ready="onEditorReady"
|
|
651
|
+
@changed="onEditorChanged"
|
|
652
|
+
>
|
|
653
|
+
<template #gutter>
|
|
654
|
+
<MinutaAnnotationsGutter
|
|
655
|
+
v-if="hasAnalysis && minuta.analysis"
|
|
656
|
+
:editor="editor"
|
|
657
|
+
:analysis="minuta.analysis"
|
|
658
|
+
:scroll-container="editorCompRef?.containerRef ?? null"
|
|
659
|
+
@open-document="onOpenDocument"
|
|
660
|
+
@update-analysis="onAnalysisUpdate"
|
|
661
|
+
/>
|
|
662
|
+
</template>
|
|
663
|
+
</MinutaDocxEditor>
|
|
664
|
+
|
|
665
|
+
<!-- edição de cabeçalho/rodapé (canvas estilo Word) -->
|
|
666
|
+
<div v-if="chromeEditor" class="minuta-embed__chrome-overlay">
|
|
667
|
+
<MinutaChromeEditor
|
|
668
|
+
:key="chromeEditor.part"
|
|
669
|
+
:part="chromeEditor.part"
|
|
670
|
+
:html="chromeText[chromeEditor.part]"
|
|
671
|
+
:scope="chromeText[chromeEditor.part === 'header' ? 'headerScope' : 'footerScope']"
|
|
672
|
+
:band-height-px="chromeEditor.part === 'header' ? props.margins.top : props.margins.bottom"
|
|
673
|
+
:show-page-number="chromeText.showPageNumber"
|
|
674
|
+
:allow-page-number-toggle="true"
|
|
675
|
+
@apply="applyChrome"
|
|
676
|
+
@close="chromeEditor = null"
|
|
677
|
+
/>
|
|
678
|
+
</div>
|
|
679
|
+
|
|
680
|
+
<!-- banner do preview de versão -->
|
|
681
|
+
<div v-if="versionPreview" class="minuta-embed__banner">
|
|
682
|
+
<span class="minuta-embed__banner-text">
|
|
683
|
+
<template v-if="compareMode">
|
|
684
|
+
Mudanças da versão {{ versionPreview.versionNumber }} em relação à original —
|
|
685
|
+
{{ compareChangeCount }} {{ compareChangeCount === 1 ? 'trecho alterado' : 'trechos alterados' }}
|
|
686
|
+
</template>
|
|
687
|
+
<template v-else>
|
|
688
|
+
Visualizando a versão {{ versionPreview.versionNumber }}
|
|
689
|
+
de {{ versionPreview.createdAt }} — somente leitura
|
|
690
|
+
</template>
|
|
691
|
+
</span>
|
|
692
|
+
<span class="minuta-embed__banner-actions">
|
|
693
|
+
<button v-if="versionPreview.versionNumber !== 1" type="button" @click="toggleCompare">
|
|
694
|
+
{{ compareMode ? 'Ocultar mudanças' : 'Ver mudanças' }}
|
|
695
|
+
</button>
|
|
696
|
+
<button type="button" :disabled="restoring" @click="restoreVersion(versionPreview.versionNumber)">
|
|
697
|
+
Restaurar
|
|
698
|
+
</button>
|
|
699
|
+
<button type="button" @click="exitPreview">
|
|
700
|
+
Voltar para a atual
|
|
701
|
+
</button>
|
|
702
|
+
</span>
|
|
703
|
+
</div>
|
|
704
|
+
</div>
|
|
705
|
+
|
|
706
|
+
<!-- painel: histórico -->
|
|
707
|
+
<aside v-if="historyOpen" class="minuta-embed__panel">
|
|
708
|
+
<div class="minuta-embed__panel-head">
|
|
709
|
+
<span>Histórico de versões</span>
|
|
710
|
+
<button type="button" @click="closeHistory">
|
|
711
|
+
×
|
|
712
|
+
</button>
|
|
713
|
+
</div>
|
|
714
|
+
<div class="minuta-embed__checkpoint">
|
|
715
|
+
<input
|
|
716
|
+
v-model="checkpointComment"
|
|
717
|
+
placeholder="Comentário (opcional)"
|
|
718
|
+
:disabled="savingCheckpoint"
|
|
719
|
+
@keydown.enter="saveCheckpoint"
|
|
720
|
+
>
|
|
721
|
+
<button type="button" :disabled="savingCheckpoint" @click="saveCheckpoint">
|
|
722
|
+
{{ savingCheckpoint ? 'Salvando…' : 'Salvar versão' }}
|
|
723
|
+
</button>
|
|
724
|
+
</div>
|
|
725
|
+
<div class="minuta-embed__versions">
|
|
726
|
+
<p v-if="!versions.length && !loadingVersions" class="minuta-embed__versions-empty">
|
|
727
|
+
Nenhuma versão ainda — salve uma com o botão acima. Versões
|
|
728
|
+
automáticas são criadas conforme você edita.
|
|
729
|
+
</p>
|
|
730
|
+
<button
|
|
731
|
+
v-for="version in versions"
|
|
732
|
+
:key="version.id"
|
|
733
|
+
class="minuta-embed__version"
|
|
734
|
+
:class="{ 'minuta-embed__version--active': versionPreview?.versionNumber === version.versionNumber }"
|
|
735
|
+
:disabled="previewLoading"
|
|
736
|
+
type="button"
|
|
737
|
+
@click="enterPreview(version.versionNumber)"
|
|
738
|
+
>
|
|
739
|
+
<span class="minuta-embed__version-row">
|
|
740
|
+
<strong>v{{ version.versionNumber }}</strong>
|
|
741
|
+
<em class="minuta-embed__version-type" :data-type="version.versionType">
|
|
742
|
+
{{ VERSION_TYPE_LABEL[version.versionType] ?? version.versionType }}
|
|
743
|
+
</em>
|
|
744
|
+
<span class="minuta-embed__version-date">{{ formatVersionDate(version.createdAt) }}</span>
|
|
745
|
+
</span>
|
|
746
|
+
<span v-if="version.comment" class="minuta-embed__version-comment">{{ version.comment }}</span>
|
|
747
|
+
<span v-if="version.createdByEmail" class="minuta-embed__version-author">{{ version.createdByEmail }}</span>
|
|
748
|
+
</button>
|
|
749
|
+
</div>
|
|
750
|
+
</aside>
|
|
751
|
+
|
|
752
|
+
<!-- assistente: launcher flutuante nativo do chat-nuxt (bolinha → janela
|
|
753
|
+
ancorada). Renderizado por ÚLTIMO: o wrapper fixed do launcher tem
|
|
754
|
+
z-index auto e ficaria atrás do editor se viesse antes no DOM. -->
|
|
755
|
+
<component
|
|
756
|
+
:is="chatEmbedComponent"
|
|
757
|
+
v-if="chatAgentId && chatEmbedComponent && minuta.status === 'READY'"
|
|
758
|
+
variant="floating"
|
|
759
|
+
launcher-label="Assistente"
|
|
760
|
+
:tela-agent-id="chatAgentId"
|
|
761
|
+
:tela-agent-inputs="chatInputs"
|
|
762
|
+
:conversation-scope="`minuta:${props.minutaId}`"
|
|
763
|
+
:custom-html-tags="['minuta-edit']"
|
|
764
|
+
:custom-components="chatComponents"
|
|
765
|
+
:features="{ showCancelButton: true }"
|
|
766
|
+
/>
|
|
767
|
+
|
|
768
|
+
<!-- visualizador de documentos (overlay) -->
|
|
769
|
+
<MinutaDocumentViewer
|
|
770
|
+
v-if="docPreviewOpen"
|
|
771
|
+
:files="minuta.files"
|
|
772
|
+
:initial-url="docPreviewUrl"
|
|
773
|
+
:initial-page="docPreviewPage"
|
|
774
|
+
@close="docPreviewOpen = false"
|
|
775
|
+
@error="msg => emit('error', msg)"
|
|
776
|
+
/>
|
|
777
|
+
</div>
|
|
778
|
+
</div>
|
|
779
|
+
</template>
|
|
780
|
+
|
|
781
|
+
<style scoped>
|
|
782
|
+
.minuta-embed{background:#f7f7f8;display:flex;flex-direction:column;height:100%}.minuta-embed__toolbar{align-items:center;background:#fff;border-bottom:1px solid #e5e7eb;display:flex;gap:8px;padding:8px 16px}.minuta-embed__name--input{background:transparent;border:0;font-family:inherit;outline:none}.minuta-embed__name--input:focus{text-decoration:underline}.minuta-embed__name{color:#111827;flex:1;font-size:15px;font-weight:500;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.minuta-embed__save-state{color:#9ca3af;font-size:12px;margin-right:4px}.minuta-embed__ghost-btn{background:#fff;border:1px solid #e5e7eb;border-radius:6px;color:#374151;cursor:pointer;font-size:13px;padding:6px 12px}.minuta-embed__ghost-btn--on{background:#f3f4f6}.minuta-embed__export{background:#111827;border:none;border-radius:6px;color:#fff;cursor:pointer;font-size:13px;padding:6px 12px}.minuta-embed__export:disabled{cursor:default;opacity:.6}.minuta-embed__chrome-overlay{bottom:16px;position:absolute;right:16px;z-index:30}.minuta-embed__status{color:#6b7280;font-size:14px;padding:48px 24px;text-align:center}.minuta-embed__status--error{color:#b91c1c}.minuta-embed__body,.minuta-embed__document{display:flex;flex:1;min-height:0}.minuta-embed__document{min-width:0;position:relative}.minuta-embed__banner{align-items:center;background:#fffbeb;border:1px solid #fcd34d;border-radius:16px;bottom:16px;box-shadow:0 4px 6px -1px rgba(0,0,0,.1);display:flex;flex-wrap:wrap;gap:8px 12px;justify-content:center;left:50%;max-width:calc(100% - 32px);padding:8px 16px;position:absolute;transform:translateX(-50%);width:-moz-max-content;width:max-content;z-index:30}.minuta-embed__banner-text{color:#92400e;font-size:12px;font-weight:500}.minuta-embed__banner-actions{display:flex;gap:8px}.minuta-embed__banner-actions button{background:#fff;border:1px solid #fcd34d;border-radius:6px;color:#92400e;cursor:pointer;font-size:12px;padding:4px 10px;white-space:nowrap}.minuta-embed__banner-actions button:disabled{cursor:default;opacity:.6}.minuta-embed__panel{background:#fff;border-left:1px solid #e5e7eb;display:flex;flex-direction:column;width:320px}.minuta-embed__panel-head{align-items:center;border-bottom:1px solid #e5e7eb;color:#111827;display:flex;font-size:14px;font-weight:600;justify-content:space-between;padding:10px 16px}.minuta-embed__panel-head button{background:none;border:none;color:#6b7280;cursor:pointer;font-size:18px;line-height:1}.minuta-embed__checkpoint{border-bottom:1px solid #e5e7eb;display:flex;flex-direction:column;gap:8px;padding:12px}.minuta-embed__checkpoint input{border:1px solid #e5e7eb;border-radius:6px;font-size:13px;padding:6px 10px}.minuta-embed__checkpoint button{background:#fff;border:1px solid #e5e7eb;border-radius:6px;cursor:pointer;font-size:13px;padding:6px 12px}.minuta-embed__versions{flex:1;min-height:0;overflow-y:auto;padding:8px}.minuta-embed__versions-empty{color:#6b7280;font-size:12px;margin:0;padding:8px 12px}.minuta-embed__version{background:#fff;border:1px solid #e5e7eb;border-radius:10px;cursor:pointer;display:block;margin-bottom:6px;padding:10px;text-align:left;width:100%}.minuta-embed__version:hover{background:#faf5ff;border-color:#c4b5fd}.minuta-embed__version--active{background:#f5f3ff;border-color:#a78bfa}.minuta-embed__version:disabled{cursor:wait;opacity:.7}.minuta-embed__version-row{align-items:center;color:#111827;display:flex;font-size:12px;gap:6px}.minuta-embed__version-type{background:#f3f4f6;border-radius:999px;color:#4b5563;font-size:11px;font-style:normal;padding:1px 8px}.minuta-embed__version-type[data-type=auto]{background:#f5f3ff;color:#6d28d9}.minuta-embed__version-type[data-type=manual]{background:#eff6ff;color:#1d4ed8}.minuta-embed__version-date{color:#9ca3af;font-size:11px;margin-left:auto}.minuta-embed__version-comment{color:#111827;display:block;font-size:12px;margin-top:4px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.minuta-embed__version-author{color:#9ca3af;display:block;font-size:11px;margin-top:2px}
|
|
783
|
+
</style>
|