tiptapify 0.1.6 → 0.2.1
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 +1 -1
- package/dist/tiptapify.css +1 -1
- package/dist/tiptapify.mjs +25792 -21937
- package/dist/tiptapify.umd.js +55 -50
- package/index.d.ts +70 -1
- package/package.json +2 -2
- package/src/components/Tiptapify.vue +38 -1
- package/src/components/Toolbar/ai.ts +13 -0
- package/src/components/Toolbar/items.ts +2 -0
- package/src/components/editorExtensions.ts +7 -3
- package/src/components/index.ts +4 -3
- package/src/extensions/components/ai/Button.vue +42 -0
- package/src/extensions/components/ai/Dialog.vue +262 -0
- package/src/extensions/components/style/StyleColor.vue +58 -12
- package/src/extensions/components/style/color/Button.vue +0 -3
- package/src/extensions/components/style/highlight/Button.vue +0 -1
- package/src/i18n/locales/ar.json +29 -0
- package/src/i18n/locales/ch.json +29 -0
- package/src/i18n/locales/cz.json +29 -0
- package/src/i18n/locales/de.json +29 -0
- package/src/i18n/locales/en.json +29 -0
- package/src/i18n/locales/es.json +29 -0
- package/src/i18n/locales/fi.json +29 -0
- package/src/i18n/locales/fr.json +29 -0
- package/src/i18n/locales/hu.json +29 -0
- package/src/i18n/locales/it.json +29 -0
- package/src/i18n/locales/ja.json +29 -0
- package/src/i18n/locales/ko.json +29 -0
- package/src/i18n/locales/la.json +29 -0
- package/src/i18n/locales/lt.json +29 -0
- package/src/i18n/locales/nl.json +29 -0
- package/src/i18n/locales/pl.json +29 -0
- package/src/i18n/locales/pt.json +29 -0
- package/src/i18n/locales/ru.json +29 -0
- package/src/i18n/locales/se.json +29 -0
- package/src/i18n/locales/th.json +29 -0
- package/src/i18n/locales/tr.json +29 -0
- package/src/i18n/locales/uk.json +29 -0
- package/src/i18n/locales/vi.json +29 -0
- package/src/index.ts +15 -1
- package/src/types/editor.ts +74 -1
- package/src/types/toolbarTypes.ts +2 -1
package/index.d.ts
CHANGED
|
@@ -32,6 +32,7 @@ import type {
|
|
|
32
32
|
SingleCommands,
|
|
33
33
|
} from '@tiptap/core'
|
|
34
34
|
|
|
35
|
+
import '@tiptap/extensions'
|
|
35
36
|
import '@tiptap/extension-text-style'
|
|
36
37
|
import '@tiptap/extension-list'
|
|
37
38
|
import '@tiptap/extensions'
|
|
@@ -67,6 +68,74 @@ export interface TiptapifyOptions {
|
|
|
67
68
|
i18n?: string
|
|
68
69
|
}
|
|
69
70
|
|
|
71
|
+
export type TiptapifyAiMode = 'insert' | 'replace' | 'append'
|
|
72
|
+
|
|
73
|
+
export interface TiptapifyAiPromptExample {
|
|
74
|
+
title: string
|
|
75
|
+
prompt: string
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export type TiptapifyAiChatRole = 'system' | 'user' | 'assistant' | 'developer' | 'tool'
|
|
79
|
+
|
|
80
|
+
export interface TiptapifyAiChatMessage {
|
|
81
|
+
role: TiptapifyAiChatRole
|
|
82
|
+
content: string
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export interface TiptapifyAiEditorContext {
|
|
86
|
+
prompt: string
|
|
87
|
+
selectedText: string
|
|
88
|
+
text: string
|
|
89
|
+
html: string
|
|
90
|
+
json: object
|
|
91
|
+
mode: TiptapifyAiMode
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export interface TiptapifyAiRequest {
|
|
95
|
+
model?: string
|
|
96
|
+
messages: TiptapifyAiChatMessage[]
|
|
97
|
+
temperature?: number
|
|
98
|
+
stream?: false
|
|
99
|
+
[key: string]: unknown
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export interface TiptapifyAiResponse {
|
|
103
|
+
content: string
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export interface TiptapifyAiOpenAiResponse {
|
|
107
|
+
choices?: Array<{
|
|
108
|
+
message?: {
|
|
109
|
+
content?: string
|
|
110
|
+
}
|
|
111
|
+
text?: string
|
|
112
|
+
}>
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export type TiptapifyAiProvider = (request: TiptapifyAiRequest, context: TiptapifyAiEditorContext) => Promise<TiptapifyAiResponse | TiptapifyAiOpenAiResponse | string>
|
|
116
|
+
|
|
117
|
+
export type TiptapifyAiTokenProvider = () => Promise<string | null> | string | null
|
|
118
|
+
|
|
119
|
+
export interface TiptapifyAiStorage {
|
|
120
|
+
getItem: (key: string) => string | null | Promise<string | null>
|
|
121
|
+
setItem: (key: string, value: string) => void | Promise<void>
|
|
122
|
+
removeItem: (key: string) => void | Promise<void>
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export interface TiptapifyAiOptions {
|
|
126
|
+
aiProvider?: TiptapifyAiProvider
|
|
127
|
+
model?: string
|
|
128
|
+
promptExamples?: TiptapifyAiPromptExample[]
|
|
129
|
+
mode?: TiptapifyAiMode
|
|
130
|
+
defaultPrompt?: string
|
|
131
|
+
systemPrompt?: string
|
|
132
|
+
temperature?: number
|
|
133
|
+
chatCompletionOptions?: Record<string, unknown>
|
|
134
|
+
createMessages?: (context: TiptapifyAiEditorContext) => TiptapifyAiChatMessage[]
|
|
135
|
+
tokenProvider?: TiptapifyAiTokenProvider
|
|
136
|
+
storage?: TiptapifyAiStorage
|
|
137
|
+
}
|
|
138
|
+
|
|
70
139
|
declare const TiptapifyPlugin: {
|
|
71
140
|
install: (app: never, options?: TiptapifyOptions) => void
|
|
72
141
|
}
|
|
@@ -115,4 +184,4 @@ declare module '@vue/runtime-core' {
|
|
|
115
184
|
interface GlobalComponents {
|
|
116
185
|
Tiptapify: typeof Tiptapify
|
|
117
186
|
}
|
|
118
|
-
}
|
|
187
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tiptapify",
|
|
3
3
|
"types": "./index.d.ts",
|
|
4
|
-
"version": "0.1
|
|
4
|
+
"version": "0.2.1",
|
|
5
5
|
"description": "Tiptap3 editor with Vuetify3 menu implementation",
|
|
6
6
|
"exports": {
|
|
7
7
|
".": {
|
|
@@ -149,4 +149,4 @@
|
|
|
149
149
|
"vue-eslint-parser": "^10.4.1",
|
|
150
150
|
"vue-tsc": "^3.3.5"
|
|
151
151
|
}
|
|
152
|
-
}
|
|
152
|
+
}
|
|
@@ -6,7 +6,7 @@ import { SlashCommandsConfig } from '@tiptapify/types/slashCommandsTypes'
|
|
|
6
6
|
import { computed, onBeforeUnmount, PropType, provide, ref, ShallowRef } from 'vue'
|
|
7
7
|
import { default as Toolbar } from '@tiptapify/components/Toolbar/Index.vue'
|
|
8
8
|
import { Editor, EditorContent } from '@tiptap/vue-3'
|
|
9
|
-
import { TiptapifyEditor, variantBtnTypes, variantFieldTypes } from '@tiptapify/types/editor'
|
|
9
|
+
import { TiptapifyAiOptions, TiptapifyEditor, variantBtnTypes, variantFieldTypes } from '@tiptapify/types/editor'
|
|
10
10
|
import MenuBubble from '@tiptapify/components/MenuBubble.vue'
|
|
11
11
|
import MenuFloating from '@tiptapify/components/MenuFloating.vue'
|
|
12
12
|
|
|
@@ -32,10 +32,12 @@ const props = defineProps({
|
|
|
32
32
|
placeholder: { type: String, default () { return '' } },
|
|
33
33
|
showWordsCount: { type: Boolean, default () { return true } },
|
|
34
34
|
showCharactersCount: { type: Boolean, default () { return true } },
|
|
35
|
+
limit: { type: Number as PropType<number | null>, default () { return null } },
|
|
35
36
|
defaultFontFamily: { type: String, default () { return 'Inter' } },
|
|
36
37
|
fontMeasure: { type: String, default () { return 'px' } },
|
|
37
38
|
rounded: { type: String, default () { return '0' } },
|
|
38
39
|
customExtensions: { type: Array as PropType<toolbarSections>, default() { return [] } },
|
|
40
|
+
ai: { type: [Boolean, Object] as PropType<boolean | TiptapifyAiOptions>, default() { return false } },
|
|
39
41
|
interactiveStyles: { type: Boolean, default() { return true } },
|
|
40
42
|
loading: { type: Boolean, default() { return false } },
|
|
41
43
|
loadingColor: { type: String, default() { return 'default' } },
|
|
@@ -49,6 +51,39 @@ const { t } = useI18n()
|
|
|
49
51
|
const appTheme = useTheme()
|
|
50
52
|
const currentTheme = ref(appTheme.global.name)
|
|
51
53
|
|
|
54
|
+
const defaultAiPromptExamples = computed(() => [
|
|
55
|
+
{
|
|
56
|
+
title: t('ai.defaultExamples.improve.title'),
|
|
57
|
+
prompt: t('ai.defaultExamples.improve.prompt'),
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
title: t('ai.defaultExamples.proofReading.title'),
|
|
61
|
+
prompt: t('ai.defaultExamples.proofReading.prompt'),
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
title: t('ai.defaultExamples.summarize.title'),
|
|
65
|
+
prompt: t('ai.defaultExamples.summarize.prompt'),
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
title: t('ai.defaultExamples.expand.title'),
|
|
69
|
+
prompt: t('ai.defaultExamples.expand.prompt'),
|
|
70
|
+
},
|
|
71
|
+
])
|
|
72
|
+
|
|
73
|
+
const tiptapifyAi = computed(() => {
|
|
74
|
+
if (props.ai === false) {
|
|
75
|
+
return false
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const options = props.ai === true ? {} : props.ai
|
|
79
|
+
|
|
80
|
+
return {
|
|
81
|
+
...options,
|
|
82
|
+
enabled: true,
|
|
83
|
+
promptExamples: options.promptExamples?.length ? options.promptExamples : defaultAiPromptExamples.value,
|
|
84
|
+
}
|
|
85
|
+
})
|
|
86
|
+
|
|
52
87
|
function contentChanged() {
|
|
53
88
|
emit('content-changed', { html: editor.value?.getHTML(), json: editor.value?.getJSON() })
|
|
54
89
|
}
|
|
@@ -58,6 +93,7 @@ const editor: ShallowRef<Editor | undefined> = getTiptapEditor(
|
|
|
58
93
|
computed(() => props.placeholder || t('content.placeholder')).value,
|
|
59
94
|
props.slashCommands,
|
|
60
95
|
props.customExtensions,
|
|
96
|
+
props.limit,
|
|
61
97
|
contentChanged,
|
|
62
98
|
(editorInstance: Editor) => {
|
|
63
99
|
(<TiptapifyEditor>editorInstance).interactiveStyles = props.interactiveStyles
|
|
@@ -73,6 +109,7 @@ const emit = defineEmits(['update:modelValue', 'editor-ready', 'content-changed'
|
|
|
73
109
|
|
|
74
110
|
provide('tiptapifyEditor', editor)
|
|
75
111
|
provide('tiptapifyI18n', { t })
|
|
112
|
+
provide('tiptapifyAi', tiptapifyAi)
|
|
76
113
|
|
|
77
114
|
editor.value?.chain().setFontFamily(props.defaultFontFamily).run()
|
|
78
115
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import actions from '@tiptapify/components/Toolbar/actions'
|
|
2
|
+
import ai from '@tiptapify/components/Toolbar/ai'
|
|
2
3
|
import alignment from '@tiptapify/components/Toolbar/alignment'
|
|
3
4
|
import format from '@tiptapify/components/Toolbar/format'
|
|
4
5
|
import formatExtra from '@tiptapify/components/Toolbar/formatExtra'
|
|
@@ -15,6 +16,7 @@ const items = {
|
|
|
15
16
|
alignment,
|
|
16
17
|
list,
|
|
17
18
|
actions,
|
|
19
|
+
ai,
|
|
18
20
|
misc,
|
|
19
21
|
}
|
|
20
22
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { VueNodeViewRenderer } from '@tiptap/vue-3'
|
|
2
|
+
import { Gapcursor } from '@tiptap/extensions'
|
|
2
3
|
import { TextStyleKit } from '@tiptap/extension-text-style'
|
|
3
4
|
import { BulletList, OrderedList, ListItem, ListKeymap, TaskList, TaskItem } from '@tiptap/extension-list'
|
|
4
5
|
import { Selection, Focus, Placeholder, UndoRedo, Dropcursor, CharacterCount } from '@tiptap/extensions'
|
|
@@ -52,8 +53,11 @@ const lowlight = createLowlight(common)
|
|
|
52
53
|
// register language example
|
|
53
54
|
// lowlight.register('ts', ts)
|
|
54
55
|
|
|
55
|
-
export function editorExtensions (placeholder: string, slashCommands: SlashCommandsConfig, customExtensions: toolbarSections) {
|
|
56
|
+
export function editorExtensions (placeholder: string, slashCommands: SlashCommandsConfig, customExtensions: toolbarSections, limit: number | null = null) {
|
|
57
|
+
const characterCount = limit && limit > 0 ? CharacterCount.configure({ limit }) : CharacterCount
|
|
58
|
+
|
|
56
59
|
const extensions = [
|
|
60
|
+
Gapcursor,
|
|
57
61
|
TextStyleKit,
|
|
58
62
|
Document,
|
|
59
63
|
Text,
|
|
@@ -109,7 +113,7 @@ export function editorExtensions (placeholder: string, slashCommands: SlashComma
|
|
|
109
113
|
Selection.configure({ className: 'selection' }),
|
|
110
114
|
TextAlign.configure({ types: ['heading', 'paragraph'] }),
|
|
111
115
|
Placeholder.configure({ placeholder }),
|
|
112
|
-
|
|
116
|
+
characterCount,
|
|
113
117
|
InvisibleCharacters.configure({
|
|
114
118
|
visible: false,
|
|
115
119
|
}),
|
|
@@ -137,4 +141,4 @@ export function editorExtensions (placeholder: string, slashCommands: SlashComma
|
|
|
137
141
|
}
|
|
138
142
|
|
|
139
143
|
return extensions
|
|
140
|
-
}
|
|
144
|
+
}
|
package/src/components/index.ts
CHANGED
|
@@ -9,15 +9,16 @@ export function getTiptapEditor (
|
|
|
9
9
|
placeholder: string,
|
|
10
10
|
slashCommands: SlashCommandsConfig = true,
|
|
11
11
|
customExtensions: toolbarSections,
|
|
12
|
+
limit: number | null = null,
|
|
12
13
|
onUpdate: Function = () => {},
|
|
13
14
|
onCreate?: (editor: Editor) => void,
|
|
14
15
|
): ShallowRef<Editor | undefined> {
|
|
15
|
-
const extensions = editorExtensions(placeholder, slashCommands, customExtensions)
|
|
16
|
+
const extensions = editorExtensions(placeholder, slashCommands, customExtensions, limit)
|
|
16
17
|
|
|
17
18
|
return useEditor({
|
|
18
19
|
content,
|
|
19
20
|
extensions,
|
|
20
21
|
onUpdate: ({ editor }) => onUpdate(),
|
|
21
|
-
onCreate: onCreate ? ({ editor }) => onCreate(editor) : undefined,
|
|
22
|
+
onCreate: onCreate ? ({ editor }) => onCreate(editor as Editor) : undefined,
|
|
22
23
|
})
|
|
23
|
-
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
<script lang="ts" setup>
|
|
2
|
+
|
|
3
|
+
import * as mdi from '@mdi/js'
|
|
4
|
+
import BtnIcon from '@tiptapify/components/UI/BtnIcon.vue'
|
|
5
|
+
import AiDialog from '@tiptapify/extensions/components/ai/Dialog.vue'
|
|
6
|
+
import defaults from '@tiptapify/constants/defaults'
|
|
7
|
+
import { TiptapifyAiResolvedOptions, variantBtnTypes } from '@tiptapify/types/editor'
|
|
8
|
+
import { computed, ComputedRef, inject, PropType, useTemplateRef } from 'vue'
|
|
9
|
+
import { ComposerTranslation } from 'vue-i18n'
|
|
10
|
+
|
|
11
|
+
defineProps({
|
|
12
|
+
variantBtn: { type: String as PropType<variantBtnTypes>, default() { return defaults.variantBtn } },
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
const { t } = inject('tiptapifyI18n') as { t: ComposerTranslation }
|
|
16
|
+
const ai = inject('tiptapifyAi') as ComputedRef<TiptapifyAiResolvedOptions | false>
|
|
17
|
+
const dialog = useTemplateRef('dialog')
|
|
18
|
+
|
|
19
|
+
const isAvailable = computed(() => Boolean(ai?.value && ai.value.aiProvider))
|
|
20
|
+
const mdiIcons = mdi as Record<string, string>
|
|
21
|
+
const icon = computed(() => `mdiSvg:${mdiIcons.mdiSparklesOutline ?? mdiIcons.mdiSparkles ?? mdi.mdiCreationOutline}`)
|
|
22
|
+
|
|
23
|
+
function showDialog() {
|
|
24
|
+
if (!isAvailable.value) {
|
|
25
|
+
return
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
dialog.value?.showDialog()
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
</script>
|
|
32
|
+
|
|
33
|
+
<template>
|
|
34
|
+
<VBtn color="" :variant="variantBtn" size="32" :disabled="!isAvailable" @click="showDialog">
|
|
35
|
+
<VTooltip activator="parent">
|
|
36
|
+
{{ isAvailable ? t('ai.title') : t('ai.unavailable') }}
|
|
37
|
+
</VTooltip>
|
|
38
|
+
<BtnIcon :icon="icon" />
|
|
39
|
+
</VBtn>
|
|
40
|
+
|
|
41
|
+
<AiDialog ref="dialog" />
|
|
42
|
+
</template>
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
<script lang="ts" setup>
|
|
2
|
+
|
|
3
|
+
import TiptapifyDialog from '@tiptapify/components/UI/TiptapifyDialog.vue'
|
|
4
|
+
import defaults from '@tiptapify/constants/defaults'
|
|
5
|
+
import {
|
|
6
|
+
TiptapifyAiEditorContext,
|
|
7
|
+
TiptapifyAiMode,
|
|
8
|
+
TiptapifyAiOpenAiResponse,
|
|
9
|
+
TiptapifyAiRequest,
|
|
10
|
+
TiptapifyAiResponse,
|
|
11
|
+
TiptapifyAiResolvedOptions,
|
|
12
|
+
TiptapifyEditor,
|
|
13
|
+
variantBtnTypes,
|
|
14
|
+
variantFieldTypes
|
|
15
|
+
} from '@tiptapify/types/editor'
|
|
16
|
+
import { computed, ComputedRef, inject, PropType, Ref, ref, useTemplateRef } from 'vue'
|
|
17
|
+
import { ComposerTranslation } from 'vue-i18n'
|
|
18
|
+
|
|
19
|
+
defineProps({
|
|
20
|
+
variantBtn: { type: String as PropType<variantBtnTypes>, default() { return defaults.variantBtn } },
|
|
21
|
+
variantField: { type: String as PropType<variantFieldTypes>, default() { return defaults.variantField } },
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
const editor = inject('tiptapifyEditor') as Ref<TiptapifyEditor>
|
|
25
|
+
const ai = inject('tiptapifyAi') as ComputedRef<TiptapifyAiResolvedOptions | false>
|
|
26
|
+
const { t } = inject('tiptapifyI18n') as { t: ComposerTranslation }
|
|
27
|
+
|
|
28
|
+
const dialog = useTemplateRef('dialog')
|
|
29
|
+
const prompt = ref('')
|
|
30
|
+
const result = ref('')
|
|
31
|
+
const error = ref('')
|
|
32
|
+
const loading = ref(false)
|
|
33
|
+
const requestRange = ref<{ from: number, to: number } | null>(null)
|
|
34
|
+
const requestMode = ref<TiptapifyAiMode>('insert')
|
|
35
|
+
|
|
36
|
+
const aiProvider = computed(() => ai?.value ? ai.value.aiProvider : undefined)
|
|
37
|
+
const promptExamples = computed(() => ai?.value ? ai.value.promptExamples : [])
|
|
38
|
+
const isGenerateDisabled = computed(() => !aiProvider.value || !prompt.value.trim() || loading.value)
|
|
39
|
+
const isApplyDisabled = computed(() => !result.value.trim() || loading.value)
|
|
40
|
+
|
|
41
|
+
defineExpose({ showDialog })
|
|
42
|
+
|
|
43
|
+
function showDialog() {
|
|
44
|
+
prompt.value = ai?.value && ai.value.defaultPrompt ? ai.value.defaultPrompt : ''
|
|
45
|
+
result.value = ''
|
|
46
|
+
error.value = ''
|
|
47
|
+
requestRange.value = null
|
|
48
|
+
requestMode.value = 'insert'
|
|
49
|
+
|
|
50
|
+
dialog.value?.open()
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function close() {
|
|
54
|
+
prompt.value = ''
|
|
55
|
+
result.value = ''
|
|
56
|
+
error.value = ''
|
|
57
|
+
requestRange.value = null
|
|
58
|
+
requestMode.value = 'insert'
|
|
59
|
+
|
|
60
|
+
dialog.value?.close()
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function applyExample(examplePrompt: string) {
|
|
64
|
+
prompt.value = examplePrompt
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function getSelectedText(from: number, to: number) {
|
|
68
|
+
if (from === to) {
|
|
69
|
+
return ''
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return editor.value.state.doc.textBetween(from, to, '\n')
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function buildDefaultMessages(context: TiptapifyAiEditorContext) {
|
|
76
|
+
const systemPrompt = ai.value && ai.value.systemPrompt
|
|
77
|
+
? ai.value.systemPrompt
|
|
78
|
+
: 'You are an assistant inside a rich text editor. Return only the final text to insert.'
|
|
79
|
+
const contextText = context.selectedText || context.text
|
|
80
|
+
const contextLabel = context.selectedText ? 'Selected text' : 'Editor text'
|
|
81
|
+
|
|
82
|
+
return [
|
|
83
|
+
{
|
|
84
|
+
role: 'system' as const,
|
|
85
|
+
content: systemPrompt,
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
role: 'user' as const,
|
|
89
|
+
content: contextText
|
|
90
|
+
? `${contextLabel}:\n${contextText}\n\nUser request:\n${context.prompt}`
|
|
91
|
+
: context.prompt,
|
|
92
|
+
},
|
|
93
|
+
]
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function buildOpenAiRequest(context: TiptapifyAiEditorContext): TiptapifyAiRequest {
|
|
97
|
+
const options = ai.value || {}
|
|
98
|
+
const request: TiptapifyAiRequest = {
|
|
99
|
+
...options.chatCompletionOptions,
|
|
100
|
+
messages: options.createMessages ? options.createMessages(context) : buildDefaultMessages(context),
|
|
101
|
+
stream: false,
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (options.model) {
|
|
105
|
+
request.model = options.model
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (typeof options.temperature === 'number') {
|
|
109
|
+
request.temperature = options.temperature
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return request
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function isTiptapifyAiResponse(response: unknown): response is TiptapifyAiResponse {
|
|
116
|
+
return typeof response === 'object' && response !== null && typeof (response as TiptapifyAiResponse).content === 'string'
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function isOpenAiResponse(response: unknown): response is TiptapifyAiOpenAiResponse {
|
|
120
|
+
return typeof response === 'object' && response !== null && Array.isArray((response as TiptapifyAiOpenAiResponse).choices)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function getResponseContent(response: TiptapifyAiResponse | TiptapifyAiOpenAiResponse | string) {
|
|
124
|
+
let content = ''
|
|
125
|
+
|
|
126
|
+
if (typeof response === 'string') {
|
|
127
|
+
content = response
|
|
128
|
+
} else if (isTiptapifyAiResponse(response)) {
|
|
129
|
+
content = response.content
|
|
130
|
+
} else if (isOpenAiResponse(response)) {
|
|
131
|
+
content = response.choices?.[0]?.message?.content ?? response.choices?.[0]?.text ?? ''
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return content.trimStart()
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
async function generate() {
|
|
138
|
+
if (isGenerateDisabled.value || !aiProvider.value) {
|
|
139
|
+
return
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
error.value = ''
|
|
143
|
+
result.value = ''
|
|
144
|
+
loading.value = true
|
|
145
|
+
|
|
146
|
+
const { from, to } = editor.value.state.selection
|
|
147
|
+
const selectedText = getSelectedText(from, to)
|
|
148
|
+
requestRange.value = selectedText ? { from, to } : null
|
|
149
|
+
requestMode.value = ai.value ? ai.value.mode ?? (selectedText ? 'replace' : 'insert') : 'insert'
|
|
150
|
+
|
|
151
|
+
const context: TiptapifyAiEditorContext = {
|
|
152
|
+
prompt: prompt.value.trim(),
|
|
153
|
+
selectedText,
|
|
154
|
+
text: selectedText || editor.value.getText(),
|
|
155
|
+
html: editor.value.getHTML(),
|
|
156
|
+
json: editor.value.getJSON(),
|
|
157
|
+
mode: requestMode.value,
|
|
158
|
+
}
|
|
159
|
+
const request = buildOpenAiRequest(context)
|
|
160
|
+
|
|
161
|
+
try {
|
|
162
|
+
const response = await aiProvider.value(request, context)
|
|
163
|
+
result.value = getResponseContent(response)
|
|
164
|
+
} catch (err) {
|
|
165
|
+
error.value = err instanceof Error ? err.message : t('ai.error')
|
|
166
|
+
} finally {
|
|
167
|
+
loading.value = false
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function apply() {
|
|
172
|
+
if (isApplyDisabled.value) {
|
|
173
|
+
return
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const chain = editor.value.chain().focus()
|
|
177
|
+
if (requestRange.value) {
|
|
178
|
+
chain.deleteRange(requestRange.value)
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (!requestRange.value && requestMode.value === 'append') {
|
|
182
|
+
chain.insertContentAt(editor.value.state.doc.content.size, result.value).run()
|
|
183
|
+
} else {
|
|
184
|
+
chain.insertContent(result.value).run()
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
close()
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
</script>
|
|
191
|
+
|
|
192
|
+
<template>
|
|
193
|
+
<TiptapifyDialog ref="dialog" module="ai" :title="t('ai.title')" :max-width="720" @close-dialog="close">
|
|
194
|
+
<template #content>
|
|
195
|
+
<VCardText>
|
|
196
|
+
<VAlert v-if="!aiProvider" type="warning" variant="tonal" density="compact" class="mb-4">
|
|
197
|
+
{{ t('ai.unavailable') }}
|
|
198
|
+
</VAlert>
|
|
199
|
+
|
|
200
|
+
<VTextarea
|
|
201
|
+
v-model="prompt"
|
|
202
|
+
:label="t('ai.prompt')"
|
|
203
|
+
:variant="variantField"
|
|
204
|
+
:disabled="loading || !aiProvider"
|
|
205
|
+
rows="4"
|
|
206
|
+
auto-grow
|
|
207
|
+
autofocus
|
|
208
|
+
/>
|
|
209
|
+
|
|
210
|
+
<div v-if="promptExamples.length" class="mb-4">
|
|
211
|
+
<VLabel class="mb-2 d-block">
|
|
212
|
+
{{ t('ai.examples') }}
|
|
213
|
+
</VLabel>
|
|
214
|
+
<div class="d-flex flex-wrap ga-2">
|
|
215
|
+
<VChip
|
|
216
|
+
v-for="example in promptExamples"
|
|
217
|
+
:key="`${example.title}:${example.prompt}`"
|
|
218
|
+
variant="tonal"
|
|
219
|
+
:disabled="loading || !aiProvider"
|
|
220
|
+
@click="applyExample(example.prompt)"
|
|
221
|
+
>
|
|
222
|
+
{{ example.title }}
|
|
223
|
+
</VChip>
|
|
224
|
+
</div>
|
|
225
|
+
</div>
|
|
226
|
+
|
|
227
|
+
<VTextarea
|
|
228
|
+
v-model="result"
|
|
229
|
+
:label="t('ai.result')"
|
|
230
|
+
:variant="variantField"
|
|
231
|
+
:loading="loading"
|
|
232
|
+
:disabled="loading"
|
|
233
|
+
rows="5"
|
|
234
|
+
auto-grow
|
|
235
|
+
/>
|
|
236
|
+
|
|
237
|
+
<VAlert v-if="loading" type="info" variant="tonal" density="compact" class="mt-4">
|
|
238
|
+
{{ t('ai.loading') }}
|
|
239
|
+
</VAlert>
|
|
240
|
+
|
|
241
|
+
<VAlert v-if="error" type="error" variant="tonal" density="compact" class="mt-4">
|
|
242
|
+
{{ error }}
|
|
243
|
+
</VAlert>
|
|
244
|
+
</VCardText>
|
|
245
|
+
</template>
|
|
246
|
+
|
|
247
|
+
<template #actions>
|
|
248
|
+
<VCardActions>
|
|
249
|
+
<VSpacer />
|
|
250
|
+
<VBtn :variant="variantBtn" @click="close">
|
|
251
|
+
{{ t('dialog.close') }}
|
|
252
|
+
</VBtn>
|
|
253
|
+
<VBtn color="primary" :variant="variantBtn" :disabled="isGenerateDisabled" :loading="loading" @click="generate">
|
|
254
|
+
{{ t('ai.generate') }}
|
|
255
|
+
</VBtn>
|
|
256
|
+
<VBtn color="primary" :variant="variantBtn" :disabled="isApplyDisabled" @click="apply">
|
|
257
|
+
{{ t('ai.apply') }}
|
|
258
|
+
</VBtn>
|
|
259
|
+
</VCardActions>
|
|
260
|
+
</template>
|
|
261
|
+
</TiptapifyDialog>
|
|
262
|
+
</template>
|
|
@@ -94,14 +94,28 @@ function hexToRgb(hex: string): Color {
|
|
|
94
94
|
function hoverColor(color: string) {
|
|
95
95
|
colorSelected.value = false
|
|
96
96
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
97
|
+
if (props.fontColor) {
|
|
98
|
+
editor.value
|
|
99
|
+
.chain()
|
|
100
|
+
.focus()
|
|
101
|
+
.setColor(color)
|
|
102
|
+
.command(({ tr }) => {
|
|
103
|
+
tr.setMeta('addToHistory', false)
|
|
104
|
+
return true
|
|
105
|
+
})
|
|
106
|
+
.run()
|
|
101
107
|
}
|
|
102
108
|
|
|
103
|
-
if (props.backgroundColor
|
|
104
|
-
editor.value
|
|
109
|
+
if (props.backgroundColor) {
|
|
110
|
+
editor.value
|
|
111
|
+
.chain()
|
|
112
|
+
.focus()
|
|
113
|
+
.setHighlight({ color })
|
|
114
|
+
.command(({ tr }) => {
|
|
115
|
+
tr.setMeta('addToHistory', false)
|
|
116
|
+
return true
|
|
117
|
+
})
|
|
118
|
+
.run()
|
|
105
119
|
}
|
|
106
120
|
}
|
|
107
121
|
|
|
@@ -115,20 +129,52 @@ function resetColor() {
|
|
|
115
129
|
}
|
|
116
130
|
|
|
117
131
|
if (props.fontColor) {
|
|
118
|
-
editor.value
|
|
132
|
+
editor.value
|
|
133
|
+
.chain()
|
|
134
|
+
.focus()
|
|
135
|
+
.setColor(initialColor.value)
|
|
136
|
+
.command(({ tr }) => {
|
|
137
|
+
tr.setMeta('addToHistory', false)
|
|
138
|
+
return true
|
|
139
|
+
})
|
|
140
|
+
.run()
|
|
119
141
|
}
|
|
120
142
|
|
|
121
143
|
if (props.backgroundColor) {
|
|
122
144
|
if (initialColor.value) {
|
|
123
|
-
editor.value
|
|
145
|
+
editor.value
|
|
146
|
+
.chain()
|
|
147
|
+
.focus()
|
|
148
|
+
.setHighlight({ color: initialColor.value })
|
|
149
|
+
.command(({ tr }) => {
|
|
150
|
+
tr.setMeta('addToHistory', false)
|
|
151
|
+
return true
|
|
152
|
+
})
|
|
153
|
+
.run()
|
|
124
154
|
} else {
|
|
125
|
-
editor.value
|
|
155
|
+
editor.value
|
|
156
|
+
.chain()
|
|
157
|
+
.focus()
|
|
158
|
+
.unsetHighlight()
|
|
159
|
+
.command(({ tr }) => {
|
|
160
|
+
tr.setMeta('addToHistory', false)
|
|
161
|
+
return true
|
|
162
|
+
})
|
|
163
|
+
.run()
|
|
126
164
|
}
|
|
127
165
|
}
|
|
128
166
|
}
|
|
129
167
|
|
|
130
|
-
function setColor() {
|
|
168
|
+
function setColor(color: string) {
|
|
131
169
|
colorSelected.value = true
|
|
170
|
+
|
|
171
|
+
if (props.fontColor) {
|
|
172
|
+
editor.value.chain().focus().setColor(color).run()
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (props.backgroundColor) {
|
|
176
|
+
editor.value.chain().focus().setHighlight({ color }).run()
|
|
177
|
+
}
|
|
132
178
|
}
|
|
133
179
|
|
|
134
180
|
function unsetColor() {
|
|
@@ -155,7 +201,7 @@ function isColorActive(color: string): boolean {
|
|
|
155
201
|
:class="isColorActive(colorCode) ? 'tiptapify-style-color-item-active' : ''"
|
|
156
202
|
@mouseenter="hoverColor(colorCode)"
|
|
157
203
|
@mouseleave="resetColor()"
|
|
158
|
-
@click="setColor"
|
|
204
|
+
@click="setColor(colorCode)"
|
|
159
205
|
>
|
|
160
206
|
<div
|
|
161
207
|
class="tiptapify-style-color-picker"
|
|
@@ -188,7 +234,7 @@ function isColorActive(color: string): boolean {
|
|
|
188
234
|
</VCardItem>
|
|
189
235
|
|
|
190
236
|
<VCardActions>
|
|
191
|
-
<VBtn variant="flat" color="primary" @click="setColor">
|
|
237
|
+
<VBtn variant="flat" color="primary" @click="setColor(customColor)">
|
|
192
238
|
OK
|
|
193
239
|
</VBtn>
|
|
194
240
|
<VBtn variant="flat" color="grey-400" @click="colorPicker = !colorPicker; customColor = initialColor; resetColor()">
|