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.
Files changed (42) hide show
  1. package/README.md +1 -1
  2. package/dist/tiptapify.css +1 -1
  3. package/dist/tiptapify.mjs +25792 -21937
  4. package/dist/tiptapify.umd.js +55 -50
  5. package/index.d.ts +70 -1
  6. package/package.json +2 -2
  7. package/src/components/Tiptapify.vue +38 -1
  8. package/src/components/Toolbar/ai.ts +13 -0
  9. package/src/components/Toolbar/items.ts +2 -0
  10. package/src/components/editorExtensions.ts +7 -3
  11. package/src/components/index.ts +4 -3
  12. package/src/extensions/components/ai/Button.vue +42 -0
  13. package/src/extensions/components/ai/Dialog.vue +262 -0
  14. package/src/extensions/components/style/StyleColor.vue +58 -12
  15. package/src/extensions/components/style/color/Button.vue +0 -3
  16. package/src/extensions/components/style/highlight/Button.vue +0 -1
  17. package/src/i18n/locales/ar.json +29 -0
  18. package/src/i18n/locales/ch.json +29 -0
  19. package/src/i18n/locales/cz.json +29 -0
  20. package/src/i18n/locales/de.json +29 -0
  21. package/src/i18n/locales/en.json +29 -0
  22. package/src/i18n/locales/es.json +29 -0
  23. package/src/i18n/locales/fi.json +29 -0
  24. package/src/i18n/locales/fr.json +29 -0
  25. package/src/i18n/locales/hu.json +29 -0
  26. package/src/i18n/locales/it.json +29 -0
  27. package/src/i18n/locales/ja.json +29 -0
  28. package/src/i18n/locales/ko.json +29 -0
  29. package/src/i18n/locales/la.json +29 -0
  30. package/src/i18n/locales/lt.json +29 -0
  31. package/src/i18n/locales/nl.json +29 -0
  32. package/src/i18n/locales/pl.json +29 -0
  33. package/src/i18n/locales/pt.json +29 -0
  34. package/src/i18n/locales/ru.json +29 -0
  35. package/src/i18n/locales/se.json +29 -0
  36. package/src/i18n/locales/th.json +29 -0
  37. package/src/i18n/locales/tr.json +29 -0
  38. package/src/i18n/locales/uk.json +29 -0
  39. package/src/i18n/locales/vi.json +29 -0
  40. package/src/index.ts +15 -1
  41. package/src/types/editor.ts +74 -1
  42. 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.6",
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
 
@@ -0,0 +1,13 @@
1
+ import { default as AiButton } from '@tiptapify/extensions/components/ai/Button.vue'
2
+ import { markRaw } from 'vue'
3
+
4
+ export default {
5
+ section: 'ai',
6
+ group: true,
7
+ components: [
8
+ {
9
+ name: 'ai',
10
+ component: markRaw(AiButton),
11
+ }
12
+ ]
13
+ }
@@ -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
- CharacterCount,
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
+ }
@@ -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
- const interactiveStyles = editor.value.interactiveStyles
98
-
99
- if (props.fontColor && interactiveStyles) {
100
- editor.value.chain().focus().setColor(color).run()
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 && interactiveStyles) {
104
- editor.value.chain().focus().setHighlight({ color: color }).run()
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.chain().focus().setColor(initialColor.value).run()
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.chain().focus().setHighlight({ color: initialColor.value }).run()
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.chain().focus().unsetHighlight().run()
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()">