memi-board 0.40.0 → 0.41.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/dist/index.js +5 -0
- package/dist/index.js.map +1 -1
- package/dist/runtime/components/Editor.vue +112 -15
- package/package.json +1 -1
|
@@ -73,6 +73,26 @@ const aiRunning = ref(false)
|
|
|
73
73
|
const aiError = ref('')
|
|
74
74
|
const aiCustomDialogOpen = ref(false)
|
|
75
75
|
const aiCustomInstruction = ref('')
|
|
76
|
+
|
|
77
|
+
interface AiPreviewPending {
|
|
78
|
+
action: WritingAssistantAction
|
|
79
|
+
result: string
|
|
80
|
+
hasSelection: boolean
|
|
81
|
+
from: number
|
|
82
|
+
to: number
|
|
83
|
+
/** true면 before/after를 일반 텍스트로, false면 HTML로 렌더링 */
|
|
84
|
+
isPlainText: boolean
|
|
85
|
+
}
|
|
86
|
+
const aiPreviewOpen = ref(false)
|
|
87
|
+
const aiPreviewBefore = ref('')
|
|
88
|
+
const aiPreviewPending = ref<AiPreviewPending | null>(null)
|
|
89
|
+
const aiPreviewLabels = computed(() => {
|
|
90
|
+
switch (aiPreviewPending.value?.action) {
|
|
91
|
+
case 'title': return { before: '현재 제목', after: '새 제목' }
|
|
92
|
+
case 'continue': return { before: '현재 내용', after: '추가될 내용' }
|
|
93
|
+
default: return { before: '이전', after: '이후' }
|
|
94
|
+
}
|
|
95
|
+
})
|
|
76
96
|
/** 관리자 전용 — 비속어 필터 건너뛰기 (테스트/공지 등 의도적 작성용) */
|
|
77
97
|
const adminSkipModeration = ref(false)
|
|
78
98
|
|
|
@@ -594,30 +614,21 @@ async function runWritingAssistant(action: WritingAssistantAction, customInstruc
|
|
|
594
614
|
? plainTextFromHtml(content.value)
|
|
595
615
|
: hasSelection ? selectedText : content.value
|
|
596
616
|
|
|
617
|
+
const isPlainText = hasSelection || action === 'title'
|
|
618
|
+
|
|
597
619
|
aiRunning.value = true
|
|
598
620
|
try {
|
|
599
621
|
const result = await assist({
|
|
600
622
|
action,
|
|
601
623
|
content: source,
|
|
602
624
|
title: title.value,
|
|
603
|
-
selection:
|
|
625
|
+
selection: isPlainText,
|
|
604
626
|
customInstruction,
|
|
605
627
|
})
|
|
606
628
|
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
else if (action === 'continue') {
|
|
611
|
-
const position = hasSelection ? to : editor.state.doc.content.size
|
|
612
|
-
editor.chain().focus().insertContentAt(position, result).run()
|
|
613
|
-
}
|
|
614
|
-
else if (hasSelection) {
|
|
615
|
-
editor.chain().focus().insertContentAt({ from, to }, result).run()
|
|
616
|
-
}
|
|
617
|
-
else {
|
|
618
|
-
editor.commands.setContent(result, { emitUpdate: true })
|
|
619
|
-
editor.chain().focus('end').run()
|
|
620
|
-
}
|
|
629
|
+
aiPreviewBefore.value = action === 'title' ? title.value : source
|
|
630
|
+
aiPreviewPending.value = { action, result, hasSelection, from, to, isPlainText }
|
|
631
|
+
aiPreviewOpen.value = true
|
|
621
632
|
}
|
|
622
633
|
catch (cause) {
|
|
623
634
|
console.warn('[memi-board] writing assistant failed', cause)
|
|
@@ -628,6 +639,35 @@ async function runWritingAssistant(action: WritingAssistantAction, customInstruc
|
|
|
628
639
|
}
|
|
629
640
|
}
|
|
630
641
|
|
|
642
|
+
function closeAiPreview() {
|
|
643
|
+
aiPreviewOpen.value = false
|
|
644
|
+
aiPreviewPending.value = null
|
|
645
|
+
aiPreviewBefore.value = ''
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
function applyAiPreview() {
|
|
649
|
+
const pending = aiPreviewPending.value
|
|
650
|
+
const editor = resolveEditor()
|
|
651
|
+
if (!pending || !editor) return
|
|
652
|
+
const { action, result, hasSelection, from, to } = pending
|
|
653
|
+
|
|
654
|
+
if (action === 'title') {
|
|
655
|
+
title.value = result.replace(/^['"“”]|['"“”]$/g, '').trim()
|
|
656
|
+
}
|
|
657
|
+
else if (action === 'continue') {
|
|
658
|
+
const position = hasSelection ? to : editor.state.doc.content.size
|
|
659
|
+
editor.chain().focus().insertContentAt(position, result).run()
|
|
660
|
+
}
|
|
661
|
+
else if (hasSelection) {
|
|
662
|
+
editor.chain().focus().insertContentAt({ from, to }, result).run()
|
|
663
|
+
}
|
|
664
|
+
else {
|
|
665
|
+
editor.commands.setContent(result, { emitUpdate: true })
|
|
666
|
+
editor.chain().focus('end').run()
|
|
667
|
+
}
|
|
668
|
+
closeAiPreview()
|
|
669
|
+
}
|
|
670
|
+
|
|
631
671
|
// 제목에서 Tab 누르면 툴바 버튼들 건너뛰고 본문으로 바로 이동
|
|
632
672
|
function focusContentFromTitle() {
|
|
633
673
|
resolveEditor()?.chain().focus().run()
|
|
@@ -1333,6 +1373,63 @@ async function handleSubmit() {
|
|
|
1333
1373
|
</div>
|
|
1334
1374
|
</template>
|
|
1335
1375
|
</UModal>
|
|
1376
|
+
|
|
1377
|
+
<UModal
|
|
1378
|
+
v-model:open="aiPreviewOpen"
|
|
1379
|
+
title="AI 결과 확인"
|
|
1380
|
+
:ui="{ content: 'sm:max-w-4xl lg:max-w-6xl' }"
|
|
1381
|
+
>
|
|
1382
|
+
<template #body>
|
|
1383
|
+
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
|
1384
|
+
<div class="flex flex-col gap-1">
|
|
1385
|
+
<span class="text-xs font-medium text-muted">{{ aiPreviewLabels.before }}</span>
|
|
1386
|
+
<div class="overflow-y-auto rounded-lg border border-default bg-elevated/30 p-3 text-sm sm:max-h-96">
|
|
1387
|
+
<p
|
|
1388
|
+
v-if="aiPreviewPending?.isPlainText"
|
|
1389
|
+
class="whitespace-pre-wrap"
|
|
1390
|
+
>{{ aiPreviewBefore }}</p>
|
|
1391
|
+
<div
|
|
1392
|
+
v-else
|
|
1393
|
+
v-html="aiPreviewBefore"
|
|
1394
|
+
/>
|
|
1395
|
+
</div>
|
|
1396
|
+
</div>
|
|
1397
|
+
<div class="flex flex-col gap-1">
|
|
1398
|
+
<span class="text-xs font-medium text-primary">{{ aiPreviewLabels.after }}</span>
|
|
1399
|
+
<div class="overflow-y-auto rounded-lg border border-primary/40 bg-primary/5 p-3 text-sm sm:max-h-96">
|
|
1400
|
+
<p
|
|
1401
|
+
v-if="aiPreviewPending?.isPlainText"
|
|
1402
|
+
class="whitespace-pre-wrap"
|
|
1403
|
+
>{{ aiPreviewPending?.result }}</p>
|
|
1404
|
+
<div
|
|
1405
|
+
v-else
|
|
1406
|
+
v-html="aiPreviewPending?.result"
|
|
1407
|
+
/>
|
|
1408
|
+
</div>
|
|
1409
|
+
</div>
|
|
1410
|
+
</div>
|
|
1411
|
+
</template>
|
|
1412
|
+
|
|
1413
|
+
<template #footer>
|
|
1414
|
+
<div class="flex w-full justify-end gap-2">
|
|
1415
|
+
<UButton
|
|
1416
|
+
type="button"
|
|
1417
|
+
color="neutral"
|
|
1418
|
+
variant="ghost"
|
|
1419
|
+
@click="closeAiPreview"
|
|
1420
|
+
>
|
|
1421
|
+
취소
|
|
1422
|
+
</UButton>
|
|
1423
|
+
<UButton
|
|
1424
|
+
type="button"
|
|
1425
|
+
icon="i-lucide-check"
|
|
1426
|
+
@click="applyAiPreview"
|
|
1427
|
+
>
|
|
1428
|
+
적용
|
|
1429
|
+
</UButton>
|
|
1430
|
+
</div>
|
|
1431
|
+
</template>
|
|
1432
|
+
</UModal>
|
|
1336
1433
|
</template>
|
|
1337
1434
|
|
|
1338
1435
|
<style scoped>
|