tiptapify 0.1.6 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/README.md +1 -1
  2. package/dist/tiptapify.css +1 -1
  3. package/dist/tiptapify.mjs +25885 -22016
  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/i18n/locales/ar.json +29 -0
  15. package/src/i18n/locales/ch.json +29 -0
  16. package/src/i18n/locales/cz.json +29 -0
  17. package/src/i18n/locales/de.json +29 -0
  18. package/src/i18n/locales/en.json +29 -0
  19. package/src/i18n/locales/es.json +29 -0
  20. package/src/i18n/locales/fi.json +29 -0
  21. package/src/i18n/locales/fr.json +29 -0
  22. package/src/i18n/locales/hu.json +29 -0
  23. package/src/i18n/locales/it.json +29 -0
  24. package/src/i18n/locales/ja.json +29 -0
  25. package/src/i18n/locales/ko.json +29 -0
  26. package/src/i18n/locales/la.json +29 -0
  27. package/src/i18n/locales/lt.json +29 -0
  28. package/src/i18n/locales/nl.json +29 -0
  29. package/src/i18n/locales/pl.json +29 -0
  30. package/src/i18n/locales/pt.json +29 -0
  31. package/src/i18n/locales/ru.json +29 -0
  32. package/src/i18n/locales/se.json +29 -0
  33. package/src/i18n/locales/th.json +29 -0
  34. package/src/i18n/locales/tr.json +29 -0
  35. package/src/i18n/locales/uk.json +29 -0
  36. package/src/i18n/locales/vi.json +29 -0
  37. package/src/index.ts +15 -1
  38. package/src/types/editor.ts +74 -1
  39. 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.0",
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>
@@ -172,5 +172,34 @@
172
172
  "footer": {
173
173
  "words": "كلمات",
174
174
  "chars": "حروف"
175
+ },
176
+ "ai": {
177
+ "title": "AI assistant",
178
+ "prompt": "Prompt",
179
+ "examples": "Examples",
180
+ "generate": "Generate",
181
+ "apply": "Apply",
182
+ "result": "Result",
183
+ "loading": "Generating...",
184
+ "unavailable": "Configure an AI provider to use this action",
185
+ "error": "AI generation failed",
186
+ "defaultExamples": {
187
+ "improve": {
188
+ "title": "Improve writing",
189
+ "prompt": "Improve the clarity and tone of this text."
190
+ },
191
+ "proofReading": {
192
+ "title": "Proof-reading",
193
+ "prompt": "Correct grammar, spelling, and punctuation without changing the meaning."
194
+ },
195
+ "summarize": {
196
+ "title": "Summarize",
197
+ "prompt": "Summarize this text in a concise paragraph."
198
+ },
199
+ "expand": {
200
+ "title": "Expand",
201
+ "prompt": "Expand this into a more detailed version."
202
+ }
203
+ }
175
204
  }
176
205
  }
@@ -172,5 +172,34 @@
172
172
  "footer": {
173
173
  "words": "单词",
174
174
  "chars": "字符"
175
+ },
176
+ "ai": {
177
+ "title": "AI assistant",
178
+ "prompt": "Prompt",
179
+ "examples": "Examples",
180
+ "generate": "Generate",
181
+ "apply": "Apply",
182
+ "result": "Result",
183
+ "loading": "Generating...",
184
+ "unavailable": "Configure an AI provider to use this action",
185
+ "error": "AI generation failed",
186
+ "defaultExamples": {
187
+ "improve": {
188
+ "title": "Improve writing",
189
+ "prompt": "Improve the clarity and tone of this text."
190
+ },
191
+ "proofReading": {
192
+ "title": "Proof-reading",
193
+ "prompt": "Correct grammar, spelling, and punctuation without changing the meaning."
194
+ },
195
+ "summarize": {
196
+ "title": "Summarize",
197
+ "prompt": "Summarize this text in a concise paragraph."
198
+ },
199
+ "expand": {
200
+ "title": "Expand",
201
+ "prompt": "Expand this into a more detailed version."
202
+ }
203
+ }
175
204
  }
176
205
  }
@@ -172,5 +172,34 @@
172
172
  "footer": {
173
173
  "words": "slova",
174
174
  "chars": "znaky"
175
+ },
176
+ "ai": {
177
+ "title": "AI assistant",
178
+ "prompt": "Prompt",
179
+ "examples": "Examples",
180
+ "generate": "Generate",
181
+ "apply": "Apply",
182
+ "result": "Result",
183
+ "loading": "Generating...",
184
+ "unavailable": "Configure an AI provider to use this action",
185
+ "error": "AI generation failed",
186
+ "defaultExamples": {
187
+ "improve": {
188
+ "title": "Improve writing",
189
+ "prompt": "Improve the clarity and tone of this text."
190
+ },
191
+ "proofReading": {
192
+ "title": "Proof-reading",
193
+ "prompt": "Correct grammar, spelling, and punctuation without changing the meaning."
194
+ },
195
+ "summarize": {
196
+ "title": "Summarize",
197
+ "prompt": "Summarize this text in a concise paragraph."
198
+ },
199
+ "expand": {
200
+ "title": "Expand",
201
+ "prompt": "Expand this into a more detailed version."
202
+ }
203
+ }
175
204
  }
176
205
  }