tiptapify 0.3.5 → 0.4.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/content.css +28 -0
- package/dist/tiptapify.css +1 -1
- package/dist/tiptapify.mjs +110770 -65623
- package/dist/tiptapify.umd.js +106 -89
- package/index.d.ts +34 -0
- package/package.json +3 -2
- package/src/components/ImageBubbleMenu.vue +5 -33
- package/src/components/MenuBubble.test.ts +120 -0
- package/src/components/MenuBubble.vue +124 -134
- package/src/components/MenuFloating.vue +48 -45
- package/src/components/Tiptapify.upload.test.ts +10 -0
- package/src/components/Tiptapify.vue +85 -45
- package/src/components/Toolbar/Index.vue +10 -0
- package/src/components/UI/BubbleRow.vue +41 -0
- package/src/components/UI/TiptapifyDialog.test.ts +37 -3
- package/src/components/UI/TiptapifyDialog.vue +15 -4
- package/src/components/editorExtensions.test.ts +9 -0
- package/src/components/editorExtensions.ts +21 -1
- package/src/composables/fakeUploadEndpoint.ts +65 -0
- package/src/composables/useTiptapifyConfig.ts +19 -0
- package/src/composables/useTiptapifyLinkPreview.test.ts +51 -0
- package/src/composables/useTiptapifyLinkPreview.ts +60 -0
- package/src/composables/useTiptapifyUpload.test.ts +96 -0
- package/src/composables/useTiptapifyUpload.ts +126 -0
- package/src/extensions/components/ai/Dialog.test.ts +335 -0
- package/src/extensions/components/ai/Dialog.vue +607 -310
- package/src/extensions/components/ai/backend.test.ts +1 -0
- package/src/extensions/components/ai/backend.ts +2 -1
- package/src/extensions/components/ai/context.test.ts +149 -0
- package/src/extensions/components/ai/context.ts +145 -0
- package/src/extensions/components/ai/diff.test.ts +65 -0
- package/src/extensions/components/ai/diff.ts +120 -0
- package/src/extensions/components/ai/errors.test.ts +45 -0
- package/src/extensions/components/ai/errors.ts +67 -0
- package/src/extensions/components/ai/result.test.ts +144 -0
- package/src/extensions/components/ai/result.ts +101 -0
- package/src/extensions/components/ai/thinking.test.ts +45 -0
- package/src/extensions/components/ai/thinking.ts +58 -0
- package/src/extensions/components/formatExtra/sub/Button.vue +1 -1
- package/src/extensions/components/formatExtra/sup/Button.vue +1 -1
- package/src/extensions/components/media/iframe/Button.vue +6 -7
- package/src/extensions/components/media/iframe/IframeDialog.test.ts +177 -0
- package/src/extensions/components/media/iframe/IframeDialog.vue +477 -105
- package/src/extensions/components/media/iframe/embed.test.ts +123 -0
- package/src/extensions/components/media/iframe/embed.ts +236 -0
- package/src/extensions/components/media/iframe/index.test.ts +123 -0
- package/src/extensions/components/media/iframe/index.ts +92 -10
- package/src/extensions/components/media/image/ImageDialog.vue +430 -132
- package/src/extensions/components/media/image/caption.test.ts +66 -1
- package/src/extensions/components/media/image/caption.ts +27 -1
- package/src/extensions/components/media/image/imageDialog.test.ts +200 -0
- package/src/extensions/components/media/image/imageDialog.ts +114 -0
- package/src/extensions/components/media/image/index.ts +2 -1
- package/src/extensions/components/media/link/Button.test.ts +96 -0
- package/src/extensions/components/media/link/Button.vue +6 -2
- package/src/extensions/components/media/link/LinkDialog.display.test.ts +363 -0
- package/src/extensions/components/media/link/LinkDialog.vue +687 -61
- package/src/extensions/components/media/link/index.ts +111 -6
- package/src/extensions/components/media/link/link.marks.test.ts +89 -0
- package/src/extensions/components/media/link/linkDialog.test.ts +296 -0
- package/src/extensions/components/media/link/linkDialog.ts +154 -0
- package/src/extensions/components/media/linkCard/PasteCardMenu.test.ts +133 -0
- package/src/extensions/components/media/linkCard/PasteCardMenu.vue +168 -0
- package/src/extensions/components/media/linkCard/index.test.ts +146 -0
- package/src/extensions/components/media/linkCard/index.ts +177 -0
- package/src/extensions/components/media/linkCard/linkCardDialog.test.ts +87 -0
- package/src/extensions/components/media/linkCard/linkCardDialog.ts +62 -0
- package/src/extensions/components/media/linkCard/linkPreview.test.ts +102 -0
- package/src/extensions/components/media/linkCard/linkPreview.ts +84 -0
- package/src/extensions/components/media/linkCard/makeLinkCard.test.ts +59 -0
- package/src/extensions/components/media/linkCard/makeLinkCard.ts +58 -0
- package/src/extensions/components/media/linkCard/pasteCard.test.ts +190 -0
- package/src/extensions/components/media/linkCard/pasteCard.ts +153 -0
- package/src/extensions/components/media/video/Button.vue +2 -1
- package/src/extensions/components/media/video/VideoDialog.test.ts +233 -0
- package/src/extensions/components/media/video/VideoDialog.vue +748 -59
- package/src/extensions/components/media/video/index.test.ts +154 -0
- package/src/extensions/components/media/video/index.ts +70 -5
- package/src/extensions/components/media/video/providers.test.ts +148 -0
- package/src/extensions/components/media/video/providers.ts +241 -0
- package/src/extensions/components/media/video/videoFile.test.ts +98 -0
- package/src/extensions/components/media/video/videoFile.ts +142 -0
- package/src/extensions/components/misc/fullscreen/Button.test.ts +55 -0
- package/src/extensions/components/misc/fullscreen/Button.vue +2 -2
- package/src/extensions/components/style/StyleColor.test.ts +255 -0
- package/src/extensions/components/style/StyleColor.vue +358 -208
- package/src/extensions/components/style/color/Button.vue +1 -3
- package/src/extensions/components/style/color/colors.test.ts +191 -0
- package/src/extensions/components/style/color/colors.ts +168 -0
- package/src/extensions/components/style/highlight/Button.vue +1 -3
- package/src/i18n/locales/ar.json +226 -23
- package/src/i18n/locales/bg.json +225 -22
- package/src/i18n/locales/ch.json +226 -23
- package/src/i18n/locales/cz.json +226 -23
- package/src/i18n/locales/de.json +226 -23
- package/src/i18n/locales/el.json +225 -22
- package/src/i18n/locales/en.json +225 -22
- package/src/i18n/locales/es.json +226 -23
- package/src/i18n/locales/et.json +225 -22
- package/src/i18n/locales/fi.json +226 -23
- package/src/i18n/locales/fr.json +226 -23
- package/src/i18n/locales/he.json +225 -22
- package/src/i18n/locales/hi.json +225 -22
- package/src/i18n/locales/hr.json +225 -22
- package/src/i18n/locales/hu.json +226 -23
- package/src/i18n/locales/id.json +225 -22
- package/src/i18n/locales/it.json +226 -23
- package/src/i18n/locales/ja.json +226 -23
- package/src/i18n/locales/ko.json +226 -23
- package/src/i18n/locales/la.json +226 -23
- package/src/i18n/locales/lt.json +226 -23
- package/src/i18n/locales/nl.json +226 -23
- package/src/i18n/locales/no.json +227 -24
- package/src/i18n/locales/pl.json +226 -23
- package/src/i18n/locales/pt-BR.json +225 -22
- package/src/i18n/locales/pt.json +226 -23
- package/src/i18n/locales/ro.json +225 -22
- package/src/i18n/locales/ru.json +227 -24
- package/src/i18n/locales/se.json +226 -23
- package/src/i18n/locales/sk.json +225 -22
- package/src/i18n/locales/sl.json +225 -22
- package/src/i18n/locales/sr.json +225 -22
- package/src/i18n/locales/th.json +226 -23
- package/src/i18n/locales/tr.json +226 -23
- package/src/i18n/locales/uk.json +227 -24
- package/src/i18n/locales/vi.json +226 -23
- package/src/index.ts +8 -0
- package/src/skins/registry.test.ts +1 -1
- package/src/skins/tokens.reachability.test.ts +1 -0
- package/src/skins/tokens.ts +3 -3
- package/src/types/editor.ts +40 -1
- package/src/utils/contentStyles.test.ts +116 -0
- package/src/utils/contentStyles.ts +175 -0
- package/src/utils/mediaSize.test.ts +22 -0
- package/src/utils/mediaSize.ts +27 -0
- package/src/utils/validateHref.test.ts +47 -2
- package/src/utils/validateHref.ts +47 -7
|
@@ -1,159 +1,235 @@
|
|
|
1
1
|
<script lang="ts" setup>
|
|
2
2
|
|
|
3
|
+
import { aiErrorDetail, aiErrorKind } from '@tiptapify/extensions/components/ai/errors'
|
|
3
4
|
import TiptapifyDialog from '@tiptapify/components/UI/TiptapifyDialog.vue'
|
|
5
|
+
import Tooltip from '@tiptapify/components/UI/Tooltip.vue'
|
|
4
6
|
import { useTiptapifyConfig } from '@tiptapify/composables/useTiptapifyConfig'
|
|
5
7
|
import {
|
|
6
8
|
TiptapifyAiEditorContext,
|
|
7
9
|
TiptapifyAiMode,
|
|
8
10
|
TiptapifyAiOpenAiResponse,
|
|
9
|
-
TiptapifyAiReasoningEffort,
|
|
10
11
|
TiptapifyAiRequest,
|
|
11
12
|
TiptapifyAiResponse,
|
|
12
13
|
TiptapifyAiResolvedOptions,
|
|
13
14
|
TiptapifyAiStream,
|
|
14
15
|
TiptapifyEditor
|
|
15
16
|
} from '@tiptapify/types/editor'
|
|
16
|
-
import { computed, ComputedRef, inject, nextTick, Ref, ref, useTemplateRef, watch } from 'vue'
|
|
17
|
+
import { computed, ComputedRef, inject, nextTick, Ref, ref, shallowRef, useTemplateRef, watch, watchEffect } from 'vue'
|
|
17
18
|
import * as mdi from '@mdi/js'
|
|
18
19
|
import { ComposerTranslation } from 'vue-i18n'
|
|
19
20
|
import { exceedsLimit } from './charLimit'
|
|
20
21
|
import { DEFAULT_AI_INSTRUCTION } from './backend'
|
|
21
|
-
|
|
22
|
-
|
|
22
|
+
import { AiContextInfo, describeAiContext, selectedTextOf, textAround, wholeWordRange } from './context'
|
|
23
|
+
import { changePercent, diffWords, formatChangePercent } from './diff'
|
|
24
|
+
import {
|
|
25
|
+
aiActions,
|
|
26
|
+
AiApplyAction,
|
|
27
|
+
AiRange,
|
|
28
|
+
applyAiResult,
|
|
29
|
+
countedText,
|
|
30
|
+
parseAiResult,
|
|
31
|
+
plainText,
|
|
32
|
+
projectedCharacters,
|
|
33
|
+
renderAiResult
|
|
34
|
+
} from './result'
|
|
35
|
+
import { AiThinkingChoice, applyThinking, initialThinkingChoice, thinkingChoices } from './thinking'
|
|
36
|
+
|
|
37
|
+
const { variantBtn } = useTiptapifyConfig()
|
|
23
38
|
|
|
24
39
|
const editor = inject('tiptapifyEditor') as Ref<TiptapifyEditor>
|
|
25
40
|
const ai = inject('tiptapifyAi') as ComputedRef<TiptapifyAiResolvedOptions | false>
|
|
26
41
|
const { t } = inject('tiptapifyI18n') as { t: ComposerTranslation }
|
|
27
42
|
|
|
28
43
|
const dialog = useTemplateRef('dialog')
|
|
44
|
+
const resultEl = useTemplateRef<HTMLElement>('resultEl')
|
|
45
|
+
const reasoningTextEl = useTemplateRef<HTMLElement>('reasoningTextEl')
|
|
46
|
+
|
|
29
47
|
const prompt = ref('')
|
|
48
|
+
// The prompt of the shown result, for "Another version" after the prompt was edited
|
|
49
|
+
const lastPrompt = ref('')
|
|
30
50
|
const result = ref('')
|
|
31
51
|
const reasoning = ref('')
|
|
32
|
-
const
|
|
33
|
-
const
|
|
52
|
+
const reasoningOpen = ref(false)
|
|
53
|
+
const reasoningStartedAt = ref<number | null>(null)
|
|
54
|
+
const reasoningEndedAt = ref<number | null>(null)
|
|
34
55
|
const error = ref('')
|
|
35
56
|
const loading = ref(false)
|
|
36
|
-
const
|
|
37
|
-
const
|
|
57
|
+
const copied = ref(false)
|
|
58
|
+
const showChanges = ref(false)
|
|
59
|
+
const requestController = shallowRef<AbortController | null>(null)
|
|
60
|
+
const requestRange = ref<AiRange | null>(null)
|
|
61
|
+
// The selected text the result was generated from, for the change percent and the diff
|
|
62
|
+
const requestText = ref('')
|
|
38
63
|
const requestMode = ref<TiptapifyAiMode>('insert')
|
|
39
|
-
const
|
|
40
|
-
const
|
|
64
|
+
const context = ref<AiContextInfo>({ kind: 'cursor', paragraphs: 0, characters: 0, excerpt: '' })
|
|
65
|
+
const thinkingChoice = ref<AiThinkingChoice | null>(null)
|
|
66
|
+
|
|
67
|
+
const aiProvider = computed(() => ai?.value ? ai.value.aiProvider : undefined)
|
|
68
|
+
const promptExamples = computed(() => ai?.value ? ai.value.promptExamples : [])
|
|
69
|
+
const showReasoning = computed(() => ai?.value?.showReasoning === true)
|
|
70
|
+
const thinkingOptions = computed(() => thinkingChoices(ai?.value || {}))
|
|
71
|
+
// With effort options the control speaks of effort ("Effort: high"), otherwise of thinking on/off
|
|
72
|
+
const effortMode = computed(() => thinkingOptions.value.length > 2)
|
|
73
|
+
const isGenerateDisabled = computed(() => !aiProvider.value || !prompt.value.trim() || loading.value)
|
|
41
74
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
75
|
+
const contextIcon = computed(() => ({
|
|
76
|
+
selection: mdi.mdiSelect,
|
|
77
|
+
document: mdi.mdiTextBoxOutline,
|
|
78
|
+
cursor: mdi.mdiCursorText,
|
|
79
|
+
})[context.value.kind])
|
|
80
|
+
const contextLabel = computed(() => {
|
|
81
|
+
const { kind, paragraphs, characters } = context.value
|
|
82
|
+
if (kind !== 'selection') {
|
|
83
|
+
return t(`ai.context.${kind}`)
|
|
45
84
|
}
|
|
46
|
-
})
|
|
47
85
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
if (element) {
|
|
52
|
-
element.scrollTop = element.scrollHeight
|
|
53
|
-
}
|
|
86
|
+
return t('ai.context.selection', {
|
|
87
|
+
paragraphs: t('ai.context.paragraphs', paragraphs),
|
|
88
|
+
characters: t('ai.context.characters', characters),
|
|
54
89
|
})
|
|
55
90
|
})
|
|
56
91
|
|
|
57
|
-
const
|
|
58
|
-
const promptExamples = computed(() => ai?.value ? ai.value.promptExamples : [])
|
|
59
|
-
const reasoningEffortOptions = computed<TiptapifyAiReasoningEffort[]>(() => {
|
|
60
|
-
const configured = ai?.value?.reasoningEffort
|
|
61
|
-
if (!configured || !Array.isArray(configured.options)) {
|
|
62
|
-
return []
|
|
63
|
-
}
|
|
92
|
+
const actions = computed(() => aiActions(context.value.kind === 'selection', ai?.value ? ai.value.mode : undefined))
|
|
64
93
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
const
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
const showReasoning = computed(() => ai?.value?.showReasoning === true)
|
|
73
|
-
const showReasoningEffort = computed(() => thinkingSupported.value && reasoningEffortSupported.value && thinkingEnabled.value)
|
|
74
|
-
const useReasoningEffort = computed(() => {
|
|
75
|
-
return thinkingSupported.value
|
|
76
|
-
&& thinkingEnabled.value
|
|
77
|
-
&& reasoningEffortSupported.value
|
|
78
|
-
&& reasoningEffort.value !== null
|
|
79
|
-
&& reasoningEffort.value !== 'default'
|
|
94
|
+
const resultFragment = computed(() => parseAiResult(result.value, editor.value.schema))
|
|
95
|
+
const resultText = computed(() => countedText(resultFragment.value))
|
|
96
|
+
const changes = computed(() => diffWords(requestText.value, plainText(resultFragment.value)))
|
|
97
|
+
const percent = computed(() => {
|
|
98
|
+
const value = loading.value ? null : changePercent(requestText.value, plainText(resultFragment.value))
|
|
99
|
+
|
|
100
|
+
return value === null ? '' : formatChangePercent(value)
|
|
80
101
|
})
|
|
81
|
-
|
|
102
|
+
|
|
82
103
|
const characterCountLimit = computed(() => {
|
|
83
104
|
const limit = getCharacterCountExtension()?.options?.limit
|
|
84
105
|
return typeof limit === 'number' ? limit : null
|
|
85
106
|
})
|
|
86
|
-
const
|
|
107
|
+
const counter = computed(() => {
|
|
108
|
+
if (characterCountLimit.value === null) {
|
|
109
|
+
return t('ai.context.characters', resultText.value.length)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const projected = projectedCharacters(editor.value.state.doc, resultText.value, actions.value[0], requestRange.value)
|
|
113
|
+
|
|
114
|
+
return `${projected} / ${characterCountLimit.value}`
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
// The CharacterCount extension rejects a result over its limit, so that action stays disabled; replacing frees the selected characters
|
|
118
|
+
function exceeds(action: AiApplyAction) {
|
|
87
119
|
const limit = characterCountLimit.value
|
|
88
120
|
if (limit === null || !result.value) {
|
|
89
121
|
return false
|
|
90
122
|
}
|
|
91
123
|
|
|
92
|
-
return exceedsLimit(
|
|
124
|
+
return exceedsLimit(resultText.value, {
|
|
93
125
|
doc: editor.value.state.doc,
|
|
94
126
|
limit,
|
|
95
|
-
mode:
|
|
127
|
+
mode: action === 'replace' ? 'replace' : 'insert',
|
|
96
128
|
range: requestRange.value,
|
|
97
129
|
})
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function canApply(action: AiApplyAction) {
|
|
133
|
+
return Boolean(result.value.trim()) && !loading.value && !exceeds(action)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const primaryExceeded = computed(() => exceeds(actions.value[0]))
|
|
137
|
+
|
|
138
|
+
// Loading shows in the result itself, not in the status line
|
|
139
|
+
const status = computed(() => {
|
|
140
|
+
if (!aiProvider.value) {
|
|
141
|
+
return { type: 'warning', text: t('ai.unavailable') }
|
|
142
|
+
}
|
|
143
|
+
if (error.value) {
|
|
144
|
+
return { type: 'error', text: error.value }
|
|
145
|
+
}
|
|
146
|
+
if (primaryExceeded.value) {
|
|
147
|
+
return { type: 'error', text: t('ai.limit_exceeded') }
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return null
|
|
98
151
|
})
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
const actionLabel = computed(() => t(`ai.${requestMode.value}`))
|
|
104
|
-
const modeOptions: TiptapifyAiMode[] = ['insert', 'replace', 'append']
|
|
105
|
-
const alternativeModes = computed(() => modeOptions.filter(mode => mode !== requestMode.value))
|
|
106
|
-
|
|
107
|
-
function applyInMode(mode: TiptapifyAiMode) {
|
|
108
|
-
if (isApplyDisabled.value) {
|
|
109
|
-
return
|
|
152
|
+
|
|
153
|
+
const reasoningLabel = computed(() => {
|
|
154
|
+
if (loading.value && reasoningEndedAt.value === null) {
|
|
155
|
+
return t('ai.thinking_now')
|
|
110
156
|
}
|
|
111
157
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
158
|
+
const started = reasoningStartedAt.value ?? 0
|
|
159
|
+
const seconds = Math.max(1, Math.round(((reasoningEndedAt.value ?? started) - started) / 1000))
|
|
160
|
+
|
|
161
|
+
return t('ai.thought_for', { seconds })
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
watch(reasoning, () => {
|
|
165
|
+
nextTick(() => {
|
|
166
|
+
const element = reasoningTextEl.value
|
|
167
|
+
if (element) {
|
|
168
|
+
element.scrollTop = element.scrollHeight
|
|
169
|
+
}
|
|
170
|
+
})
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
watchEffect(() => {
|
|
174
|
+
if (resultEl.value) {
|
|
175
|
+
renderAiResult(resultFragment.value, editor.value.schema, resultEl.value)
|
|
176
|
+
}
|
|
177
|
+
}, { flush: 'post' })
|
|
115
178
|
|
|
116
179
|
defineExpose({ showDialog })
|
|
117
180
|
|
|
118
|
-
function
|
|
119
|
-
|
|
120
|
-
|
|
181
|
+
function actionLabel(action: AiApplyAction) {
|
|
182
|
+
return t({ replace: 'ai.replace_selection', below: 'ai.insert_below', insert: 'ai.insert', append: 'ai.append' }[action])
|
|
183
|
+
}
|
|
121
184
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
185
|
+
// A menu item ("High") and the control's own label ("Effort: high"): with effort options
|
|
186
|
+
// thinking on is the provider's default effort
|
|
187
|
+
function choiceLabel(choice: AiThinkingChoice, group: 'states' | 'label' = 'states') {
|
|
188
|
+
if (choice === 'off' || (choice === 'on' && !effortMode.value)) {
|
|
189
|
+
return t(`ai.thinking_${group}.${choice}`)
|
|
125
190
|
}
|
|
126
191
|
|
|
127
|
-
|
|
128
|
-
? configured.default
|
|
129
|
-
: 'default'
|
|
192
|
+
return t(`ai.effort_${group === 'states' ? 'levels' : 'label'}.${choice === 'on' ? 'default' : choice}`)
|
|
130
193
|
}
|
|
131
194
|
|
|
132
|
-
function
|
|
133
|
-
prompt.value = ai?.value && ai.value.defaultPrompt ? ai.value.defaultPrompt : ''
|
|
195
|
+
function resetResult() {
|
|
134
196
|
result.value = ''
|
|
135
197
|
reasoning.value = ''
|
|
198
|
+
reasoningOpen.value = false
|
|
199
|
+
reasoningStartedAt.value = null
|
|
200
|
+
reasoningEndedAt.value = null
|
|
136
201
|
error.value = ''
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
requestMode.value = ai.value?.mode ?? (getSelectedText(from, to) ? 'replace' : 'insert')
|
|
140
|
-
thinkingEnabled.value = true
|
|
141
|
-
resetReasoningEffort()
|
|
142
|
-
|
|
143
|
-
dialog.value?.open()
|
|
202
|
+
copied.value = false
|
|
203
|
+
showChanges.value = false
|
|
144
204
|
}
|
|
145
205
|
|
|
146
|
-
function
|
|
206
|
+
function reset() {
|
|
147
207
|
requestController.value?.abort()
|
|
148
208
|
requestController.value = null
|
|
209
|
+
loading.value = false
|
|
149
210
|
|
|
150
211
|
prompt.value = ''
|
|
151
|
-
|
|
152
|
-
reasoning.value = ''
|
|
153
|
-
error.value = ''
|
|
212
|
+
lastPrompt.value = ''
|
|
154
213
|
requestRange.value = null
|
|
214
|
+
requestText.value = ''
|
|
155
215
|
requestMode.value = 'insert'
|
|
216
|
+
resetResult()
|
|
217
|
+
}
|
|
156
218
|
|
|
219
|
+
function showDialog() {
|
|
220
|
+
reset()
|
|
221
|
+
prompt.value = ai?.value && ai.value.defaultPrompt ? ai.value.defaultPrompt : ''
|
|
222
|
+
|
|
223
|
+
const { doc, selection } = editor.value.state
|
|
224
|
+
const { from, to } = wholeWordRange(doc, selection.from, selection.to)
|
|
225
|
+
context.value = describeAiContext(doc, from, to)
|
|
226
|
+
requestMode.value = (ai?.value ? ai.value.mode : undefined) ?? (selectedTextOf(doc, from, to) ? 'replace' : 'insert')
|
|
227
|
+
thinkingChoice.value = initialThinkingChoice(ai?.value || {})
|
|
228
|
+
|
|
229
|
+
dialog.value?.open()
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function cancel() {
|
|
157
233
|
dialog.value?.close()
|
|
158
234
|
}
|
|
159
235
|
|
|
@@ -165,20 +241,20 @@ function applyExample(examplePrompt: string) {
|
|
|
165
241
|
prompt.value = examplePrompt
|
|
166
242
|
}
|
|
167
243
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
244
|
+
// Enter sends, Shift+Enter is a new line (an IME composition keeps its Enter)
|
|
245
|
+
function onPromptKeydown(event: KeyboardEvent) {
|
|
246
|
+
if (event.key !== 'Enter' || event.shiftKey || event.isComposing) {
|
|
247
|
+
return
|
|
171
248
|
}
|
|
172
249
|
|
|
173
|
-
|
|
250
|
+
event.preventDefault()
|
|
251
|
+
generate()
|
|
174
252
|
}
|
|
175
253
|
|
|
176
254
|
function buildDefaultMessages(context: TiptapifyAiEditorContext) {
|
|
177
255
|
const systemPrompt = ai.value && ai.value.systemPrompt
|
|
178
256
|
? ai.value.systemPrompt
|
|
179
257
|
: DEFAULT_AI_INSTRUCTION
|
|
180
|
-
const contextText = context.selectedText || context.text
|
|
181
|
-
const contextLabel = context.selectedText ? 'Selected text' : 'Editor text'
|
|
182
258
|
|
|
183
259
|
return [
|
|
184
260
|
{
|
|
@@ -187,13 +263,28 @@ function buildDefaultMessages(context: TiptapifyAiEditorContext) {
|
|
|
187
263
|
},
|
|
188
264
|
{
|
|
189
265
|
role: 'user' as const,
|
|
190
|
-
content:
|
|
191
|
-
? `${contextLabel}:\n${contextText}\n\nUser request:\n${context.prompt}`
|
|
192
|
-
: context.prompt,
|
|
266
|
+
content: defaultUserMessage(context),
|
|
193
267
|
},
|
|
194
268
|
]
|
|
195
269
|
}
|
|
196
270
|
|
|
271
|
+
// A selection goes with the text around it and the rule to answer for the selection alone: given only a
|
|
272
|
+
// fragment, a model takes it for the whole text and completes it, and the completion lands next to the rest
|
|
273
|
+
function defaultUserMessage(context: TiptapifyAiEditorContext): string {
|
|
274
|
+
if (!context.selectedText) {
|
|
275
|
+
return context.text ? `Editor text:\n${context.text}\n\nUser request:\n${context.prompt}` : context.prompt
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
return [
|
|
279
|
+
context.textBefore ? `Text before the selection:\n${context.textBefore}` : '',
|
|
280
|
+
`Selected text:\n${context.selectedText}`,
|
|
281
|
+
context.textAfter ? `Text after the selection:\n${context.textAfter}` : '',
|
|
282
|
+
'Work on the selected text only and answer with its new version alone. The text before and after it stays '
|
|
283
|
+
+ 'in the document as it is: do not repeat it and do not continue the selection into it.',
|
|
284
|
+
`User request:\n${context.prompt}`,
|
|
285
|
+
].filter(Boolean).join('\n\n')
|
|
286
|
+
}
|
|
287
|
+
|
|
197
288
|
function buildOpenAiRequest(context: TiptapifyAiEditorContext): TiptapifyAiRequest {
|
|
198
289
|
const options = ai.value || {}
|
|
199
290
|
const chatCompletionOptions = options.chatCompletionOptions ?? {}
|
|
@@ -212,16 +303,7 @@ function buildOpenAiRequest(context: TiptapifyAiEditorContext): TiptapifyAiReque
|
|
|
212
303
|
request.temperature = options.temperature
|
|
213
304
|
}
|
|
214
305
|
|
|
215
|
-
|
|
216
|
-
delete request.enable_thinking
|
|
217
|
-
request.reasoning_effort = reasoningEffort.value
|
|
218
|
-
} else {
|
|
219
|
-
delete request.reasoning_effort
|
|
220
|
-
|
|
221
|
-
request.enable_thinking = thinkingSupported.value && thinkingEnabled.value
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
return request
|
|
306
|
+
return applyThinking(request, thinkingChoice.value)
|
|
225
307
|
}
|
|
226
308
|
|
|
227
309
|
function isTiptapifyAiResponse(response: unknown): response is TiptapifyAiResponse {
|
|
@@ -246,26 +328,48 @@ function getResponseContent(response: TiptapifyAiResponse | TiptapifyAiOpenAiRes
|
|
|
246
328
|
return content.trimStart()
|
|
247
329
|
}
|
|
248
330
|
|
|
249
|
-
|
|
250
|
-
|
|
331
|
+
// Thinking ends with the first chunk of the answer (or with the request)
|
|
332
|
+
function markThoughtEnd() {
|
|
333
|
+
if (reasoningStartedAt.value !== null && reasoningEndedAt.value === null) {
|
|
334
|
+
reasoningEndedAt.value = Date.now()
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
function errorMessage(err: unknown): string {
|
|
340
|
+
const kind = aiErrorKind(err)
|
|
341
|
+
const detail = aiErrorDetail(err)
|
|
342
|
+
|
|
343
|
+
if (kind === 'other') {
|
|
344
|
+
return detail || t('ai.error')
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
return detail ? `${t(`ai.errors.${kind}`)} ${t('ai.errors.service_says')} ${detail}` : t(`ai.errors.${kind}`)
|
|
348
|
+
}
|
|
349
|
+
async function generate(promptText = prompt.value.trim()) {
|
|
350
|
+
if (!aiProvider.value || loading.value || !promptText) {
|
|
251
351
|
return
|
|
252
352
|
}
|
|
253
353
|
|
|
254
|
-
|
|
255
|
-
result.value = ''
|
|
256
|
-
reasoning.value = ''
|
|
354
|
+
resetResult()
|
|
257
355
|
loading.value = true
|
|
356
|
+
lastPrompt.value = promptText
|
|
258
357
|
|
|
259
358
|
const controller = new AbortController()
|
|
260
359
|
requestController.value = controller
|
|
261
360
|
|
|
262
|
-
const {
|
|
263
|
-
const
|
|
361
|
+
const { doc, selection } = editor.value.state
|
|
362
|
+
const { from, to } = wholeWordRange(doc, selection.from, selection.to)
|
|
363
|
+
const selectedText = selectedTextOf(doc, from, to)
|
|
264
364
|
requestRange.value = selectedText ? { from, to } : null
|
|
365
|
+
requestText.value = selectedText
|
|
265
366
|
|
|
367
|
+
const around = textAround(doc, from, to)
|
|
266
368
|
const context: TiptapifyAiEditorContext = {
|
|
267
|
-
prompt:
|
|
369
|
+
prompt: promptText,
|
|
268
370
|
selectedText,
|
|
371
|
+
textBefore: around.before,
|
|
372
|
+
textAfter: around.after,
|
|
269
373
|
text: selectedText || editor.value.getText(),
|
|
270
374
|
html: editor.value.getHTML(),
|
|
271
375
|
json: editor.value.getJSON(),
|
|
@@ -277,11 +381,13 @@ async function generate() {
|
|
|
277
381
|
signal: controller.signal,
|
|
278
382
|
onChunk: (chunk: string) => {
|
|
279
383
|
if (chunk && !controller.signal.aborted) {
|
|
384
|
+
markThoughtEnd()
|
|
280
385
|
result.value += chunk
|
|
281
386
|
}
|
|
282
387
|
},
|
|
283
388
|
onReasoning: (chunk: string) => {
|
|
284
389
|
if (chunk && !controller.signal.aborted) {
|
|
390
|
+
reasoningStartedAt.value ??= Date.now()
|
|
285
391
|
reasoning.value += chunk
|
|
286
392
|
}
|
|
287
393
|
},
|
|
@@ -296,10 +402,15 @@ async function generate() {
|
|
|
296
402
|
}
|
|
297
403
|
} catch (err) {
|
|
298
404
|
if (!controller.signal.aborted) {
|
|
299
|
-
error.value = err
|
|
405
|
+
error.value = errorMessage(err)
|
|
300
406
|
}
|
|
301
407
|
} finally {
|
|
302
|
-
|
|
408
|
+
// A request the dialog already let go of (closed meanwhile) leaves the state alone
|
|
409
|
+
if (requestController.value === controller) {
|
|
410
|
+
loading.value = false
|
|
411
|
+
requestController.value = null
|
|
412
|
+
markThoughtEnd()
|
|
413
|
+
}
|
|
303
414
|
}
|
|
304
415
|
}
|
|
305
416
|
|
|
@@ -307,262 +418,448 @@ function getCharacterCountExtension() {
|
|
|
307
418
|
return editor.value.options.extensions?.find(item => item.name === 'characterCount')
|
|
308
419
|
}
|
|
309
420
|
|
|
310
|
-
function
|
|
311
|
-
|
|
421
|
+
async function copyResult() {
|
|
422
|
+
const container = document.createElement('div')
|
|
423
|
+
renderAiResult(resultFragment.value, editor.value.schema, container)
|
|
424
|
+
const text = plainText(resultFragment.value)
|
|
425
|
+
|
|
426
|
+
try {
|
|
427
|
+
if (typeof ClipboardItem === 'undefined') {
|
|
428
|
+
await navigator.clipboard.writeText(text)
|
|
429
|
+
} else {
|
|
430
|
+
await navigator.clipboard.write([new ClipboardItem({
|
|
431
|
+
'text/html': new Blob([container.innerHTML], { type: 'text/html' }),
|
|
432
|
+
'text/plain': new Blob([text], { type: 'text/plain' }),
|
|
433
|
+
})])
|
|
434
|
+
}
|
|
435
|
+
} catch {
|
|
436
|
+
// No clipboard access (an insecure page or a denied permission): the button stays as it was
|
|
312
437
|
return
|
|
313
438
|
}
|
|
314
439
|
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
}
|
|
440
|
+
copied.value = true
|
|
441
|
+
setTimeout(() => {
|
|
442
|
+
copied.value = false
|
|
443
|
+
}, 1500)
|
|
444
|
+
}
|
|
319
445
|
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
chain.insertContent(result.value).run()
|
|
446
|
+
function apply(action: AiApplyAction) {
|
|
447
|
+
if (!canApply(action)) {
|
|
448
|
+
return
|
|
324
449
|
}
|
|
325
450
|
|
|
326
|
-
|
|
451
|
+
applyAiResult(editor.value, action, result.value, requestRange.value)
|
|
452
|
+
dialog.value?.close()
|
|
327
453
|
}
|
|
328
454
|
|
|
329
455
|
</script>
|
|
330
456
|
|
|
331
457
|
<template>
|
|
332
|
-
<TiptapifyDialog ref="dialog" module="ai" :title="t('ai.title')" :max-width="
|
|
458
|
+
<TiptapifyDialog ref="dialog" module="ai" :title="t('ai.title')" :max-width="640" @submit="apply(actions[0])" @closed="reset">
|
|
459
|
+
<template #title>
|
|
460
|
+
<VIcon size="20" class="mr-2" :icon="`mdiSvg:${mdi.mdiCreation}`" />{{ t('ai.title') }}
|
|
461
|
+
</template>
|
|
462
|
+
|
|
333
463
|
<template #content>
|
|
334
|
-
<VCardText>
|
|
335
|
-
<
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
{{ t('ai.quick_prompts') }}
|
|
342
|
-
</VLabel>
|
|
343
|
-
<div class="d-flex flex-wrap ga-2">
|
|
344
|
-
<VChip
|
|
345
|
-
v-for="example in promptExamples"
|
|
346
|
-
:key="`${example.title}:${example.prompt}`"
|
|
347
|
-
variant="tonal"
|
|
348
|
-
:disabled="loading || !aiProvider"
|
|
349
|
-
@click="applyExample(example.prompt)"
|
|
350
|
-
>
|
|
351
|
-
{{ example.title }}
|
|
352
|
-
</VChip>
|
|
353
|
-
</div>
|
|
464
|
+
<VCardText class="ai-dialog">
|
|
465
|
+
<div class="ai-context">
|
|
466
|
+
<VIcon size="16" :icon="`mdiSvg:${contextIcon}`" />
|
|
467
|
+
<span class="ai-context__label">{{ contextLabel }}</span>
|
|
468
|
+
<span v-if="context.excerpt" class="ai-context__excerpt">
|
|
469
|
+
{{ t('ai.context.excerpt', { text: context.excerpt }) }}
|
|
470
|
+
</span>
|
|
354
471
|
</div>
|
|
355
472
|
|
|
356
|
-
<
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
<VIcon size="x-small" class="mr-2" :icon="`mdiSvg:${mdi.mdiBrain}`" />
|
|
368
|
-
<VLabel>{{ t('ai.thinking') }}</VLabel>
|
|
369
|
-
<VSwitch
|
|
370
|
-
v-model="thinkingEnabled"
|
|
371
|
-
:disabled="loading || useReasoningEffort"
|
|
372
|
-
density="compact"
|
|
373
|
-
hide-details
|
|
374
|
-
color="primary"
|
|
375
|
-
class="ai-thinking-toggle__switch"
|
|
376
|
-
/>
|
|
473
|
+
<div v-if="promptExamples.length" class="ai-examples" role="group" :aria-label="t('ai.quick_prompts')">
|
|
474
|
+
<VChip
|
|
475
|
+
v-for="example in promptExamples"
|
|
476
|
+
:key="`${example.title}:${example.prompt}`"
|
|
477
|
+
size="small"
|
|
478
|
+
variant="tonal"
|
|
479
|
+
:disabled="loading || !aiProvider"
|
|
480
|
+
@click="applyExample(example.prompt)"
|
|
481
|
+
>
|
|
482
|
+
{{ example.title }}
|
|
483
|
+
</VChip>
|
|
377
484
|
</div>
|
|
378
485
|
|
|
379
|
-
<div
|
|
380
|
-
<
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
:
|
|
385
|
-
:disabled="loading"
|
|
386
|
-
|
|
486
|
+
<div class="ai-prompt" :class="{ 'ai-prompt--disabled': !aiProvider }">
|
|
487
|
+
<VTextarea
|
|
488
|
+
v-model="prompt"
|
|
489
|
+
class="ai-prompt__input"
|
|
490
|
+
:aria-label="t('ai.prompt')"
|
|
491
|
+
:placeholder="t('ai.prompt_placeholder')"
|
|
492
|
+
:disabled="loading || !aiProvider"
|
|
493
|
+
variant="plain"
|
|
387
494
|
density="compact"
|
|
495
|
+
rows="2"
|
|
496
|
+
max-rows="8"
|
|
388
497
|
hide-details
|
|
389
|
-
|
|
498
|
+
auto-grow
|
|
499
|
+
autofocus
|
|
500
|
+
@keydown="onPromptKeydown"
|
|
390
501
|
/>
|
|
502
|
+
|
|
503
|
+
<div class="ai-prompt__bar">
|
|
504
|
+
<VMenu v-if="thinkingChoice" location="top start">
|
|
505
|
+
<template #activator="{ props }">
|
|
506
|
+
<VBtn
|
|
507
|
+
v-bind="props"
|
|
508
|
+
class="ai-thinking"
|
|
509
|
+
size="small"
|
|
510
|
+
variant="text"
|
|
511
|
+
:disabled="loading || !aiProvider"
|
|
512
|
+
:prepend-icon="`mdiSvg:${mdi.mdiBrain}`"
|
|
513
|
+
:append-icon="`mdiSvg:${mdi.mdiChevronDown}`"
|
|
514
|
+
>
|
|
515
|
+
{{ choiceLabel(thinkingChoice, 'label') }}
|
|
516
|
+
</VBtn>
|
|
517
|
+
</template>
|
|
518
|
+
<VList density="compact">
|
|
519
|
+
<VListSubheader>{{ effortMode ? t('ai.reasoning_effort') : t('ai.thinking') }}</VListSubheader>
|
|
520
|
+
<VListItem
|
|
521
|
+
v-for="choice in thinkingOptions"
|
|
522
|
+
:key="choice"
|
|
523
|
+
:active="choice === thinkingChoice"
|
|
524
|
+
color="primary"
|
|
525
|
+
@click="thinkingChoice = choice"
|
|
526
|
+
>
|
|
527
|
+
<VListItemTitle>{{ choiceLabel(choice) }}</VListItemTitle>
|
|
528
|
+
</VListItem>
|
|
529
|
+
</VList>
|
|
530
|
+
</VMenu>
|
|
531
|
+
|
|
532
|
+
<VSpacer />
|
|
533
|
+
|
|
534
|
+
<span class="ai-prompt__hint">{{ t('ai.send_hint') }}</span>
|
|
535
|
+
|
|
536
|
+
<VBtn
|
|
537
|
+
v-if="loading"
|
|
538
|
+
class="ai-send ai-send--stop"
|
|
539
|
+
icon
|
|
540
|
+
size="32"
|
|
541
|
+
variant="tonal"
|
|
542
|
+
:aria-label="t('ai.stop')"
|
|
543
|
+
@click="stop"
|
|
544
|
+
>
|
|
545
|
+
<VIcon size="18" :icon="`mdiSvg:${mdi.mdiStop}`" />
|
|
546
|
+
<Tooltip :label="t('ai.stop')" />
|
|
547
|
+
</VBtn>
|
|
548
|
+
<VBtn
|
|
549
|
+
v-else
|
|
550
|
+
class="ai-send"
|
|
551
|
+
icon
|
|
552
|
+
size="32"
|
|
553
|
+
color="primary"
|
|
554
|
+
variant="flat"
|
|
555
|
+
:disabled="isGenerateDisabled"
|
|
556
|
+
:aria-label="t('ai.generate')"
|
|
557
|
+
@click="generate()"
|
|
558
|
+
>
|
|
559
|
+
<VIcon size="18" :icon="`mdiSvg:${mdi.mdiArrowUp}`" />
|
|
560
|
+
<Tooltip :label="t('ai.generate')" />
|
|
561
|
+
</VBtn>
|
|
562
|
+
</div>
|
|
563
|
+
</div>
|
|
564
|
+
|
|
565
|
+
<div v-if="status" class="ai-status" :class="`ai-status--${status.type}`" role="alert">
|
|
566
|
+
<VIcon size="16" :icon="`mdiSvg:${mdi.mdiAlertCircleOutline}`" />
|
|
567
|
+
<span>{{ status.text }}</span>
|
|
568
|
+
</div>
|
|
569
|
+
|
|
570
|
+
<div v-if="showReasoning && reasoning" class="ai-reasoning">
|
|
571
|
+
<button
|
|
572
|
+
type="button"
|
|
573
|
+
class="ai-reasoning__toggle"
|
|
574
|
+
:aria-expanded="reasoningOpen"
|
|
575
|
+
@click="reasoningOpen = !reasoningOpen"
|
|
576
|
+
>
|
|
577
|
+
<VIcon size="16" :icon="`mdiSvg:${mdi.mdiBrain}`" />
|
|
578
|
+
<span>{{ reasoningLabel }}</span>
|
|
579
|
+
<VIcon size="16" :icon="`mdiSvg:${reasoningOpen ? mdi.mdiChevronDown : mdi.mdiChevronRight}`" />
|
|
580
|
+
</button>
|
|
581
|
+
<pre v-if="reasoningOpen" ref="reasoningTextEl" class="ai-reasoning__text">{{ reasoning }}</pre>
|
|
391
582
|
</div>
|
|
392
583
|
|
|
393
|
-
<
|
|
394
|
-
<
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
584
|
+
<div v-if="result || loading" class="ai-result" :aria-busy="loading">
|
|
585
|
+
<VProgressLinear v-if="loading" class="ai-result__progress" color="primary" height="2" indeterminate />
|
|
586
|
+
|
|
587
|
+
<div v-if="!result" class="ai-result__placeholder">
|
|
588
|
+
{{ t('ai.loading') }}
|
|
589
|
+
</div>
|
|
590
|
+
<div v-else-if="showChanges" class="ai-result__content ai-diff">
|
|
591
|
+
<span v-for="(segment, index) in changes" :key="index" :class="`ai-diff__${segment.type}`">{{ segment.text }}</span>
|
|
592
|
+
</div>
|
|
593
|
+
<div v-else ref="resultEl" class="ai-result__content" role="region" :aria-label="t('ai.result')" />
|
|
594
|
+
|
|
595
|
+
<div v-if="result" class="ai-result__footer">
|
|
596
|
+
<span v-if="percent" class="ai-result__meta">{{ t('ai.change_vs_original', { percent }) }}</span>
|
|
597
|
+
<span class="ai-result__meta ai-result__counter" :class="{ 'ai-result__meta--exceeded': primaryExceeded }">{{ counter }}</span>
|
|
598
|
+
|
|
599
|
+
<VSpacer />
|
|
600
|
+
|
|
601
|
+
<VBtn
|
|
602
|
+
v-if="requestText"
|
|
603
|
+
size="small"
|
|
604
|
+
variant="text"
|
|
605
|
+
:active="showChanges"
|
|
606
|
+
:disabled="loading"
|
|
607
|
+
:prepend-icon="`mdiSvg:${mdi.mdiCompareHorizontal}`"
|
|
608
|
+
@click="showChanges = !showChanges"
|
|
609
|
+
>
|
|
610
|
+
{{ t('ai.changes') }}
|
|
611
|
+
</VBtn>
|
|
612
|
+
<VBtn
|
|
613
|
+
size="small"
|
|
614
|
+
variant="text"
|
|
615
|
+
:disabled="loading || !aiProvider"
|
|
616
|
+
:prepend-icon="`mdiSvg:${mdi.mdiRefresh}`"
|
|
617
|
+
@click="generate(lastPrompt)"
|
|
618
|
+
>
|
|
619
|
+
{{ t('ai.another_version') }}
|
|
620
|
+
</VBtn>
|
|
621
|
+
<VBtn
|
|
622
|
+
size="small"
|
|
623
|
+
variant="text"
|
|
624
|
+
:disabled="loading"
|
|
625
|
+
:prepend-icon="`mdiSvg:${copied ? mdi.mdiCheck : mdi.mdiContentCopy}`"
|
|
626
|
+
@click="copyResult"
|
|
627
|
+
>
|
|
628
|
+
{{ copied ? t('ai.copied') : t('ai.copy') }}
|
|
629
|
+
</VBtn>
|
|
630
|
+
</div>
|
|
631
|
+
</div>
|
|
440
632
|
</VCardText>
|
|
441
633
|
</template>
|
|
442
634
|
|
|
443
635
|
<template #actions>
|
|
444
636
|
<VCardActions>
|
|
445
|
-
<
|
|
446
|
-
|
|
447
|
-
{{ t('dialog.close') }}
|
|
637
|
+
<VBtn :variant="variantBtn" @click="cancel">
|
|
638
|
+
{{ t('dialog.cancel') }}
|
|
448
639
|
</VBtn>
|
|
449
|
-
|
|
450
|
-
|
|
640
|
+
|
|
641
|
+
<VSpacer />
|
|
642
|
+
|
|
643
|
+
<VBtn :variant="variantBtn" :disabled="!canApply(actions[1])" @click="apply(actions[1])">
|
|
644
|
+
{{ actionLabel(actions[1]) }}
|
|
451
645
|
</VBtn>
|
|
452
|
-
<VBtn
|
|
453
|
-
{{
|
|
646
|
+
<VBtn type="submit" color="primary" :variant="variantBtn" :disabled="!canApply(actions[0])">
|
|
647
|
+
{{ actionLabel(actions[0]) }}
|
|
454
648
|
</VBtn>
|
|
455
|
-
|
|
456
|
-
<VBtnGroup
|
|
457
|
-
:class="{ 'tiptapify-btn-group--elevated': variantBtn === 'elevated' && !isApplyDisabled }"
|
|
458
|
-
density="compact"
|
|
459
|
-
:variant="variantBtn"
|
|
460
|
-
divided
|
|
461
|
-
>
|
|
462
|
-
<VBtn
|
|
463
|
-
color="primary"
|
|
464
|
-
:variant="variantBtn"
|
|
465
|
-
:disabled="isApplyDisabled"
|
|
466
|
-
@click="apply"
|
|
467
|
-
>
|
|
468
|
-
{{ t('ai.insert') }}
|
|
469
|
-
</VBtn>
|
|
470
|
-
|
|
471
|
-
<VMenu>
|
|
472
|
-
<template #activator="{ props }">
|
|
473
|
-
<VBtn
|
|
474
|
-
v-bind="{ ...props, minWidth: 30 }"
|
|
475
|
-
color="primary"
|
|
476
|
-
:disabled="isApplyDisabled"
|
|
477
|
-
:variant="variantBtn"
|
|
478
|
-
>
|
|
479
|
-
<VIcon :icon="`mdiSvg:${mdi.mdiDotsVertical}`" />
|
|
480
|
-
</VBtn>
|
|
481
|
-
</template>
|
|
482
|
-
<VList>
|
|
483
|
-
<VListItem
|
|
484
|
-
v-for="mode in alternativeModes"
|
|
485
|
-
:key="mode"
|
|
486
|
-
@click="applyInMode(mode)"
|
|
487
|
-
>
|
|
488
|
-
<VListItemTitle>{{ t(`ai.${mode}`) }}</VListItemTitle>
|
|
489
|
-
</VListItem>
|
|
490
|
-
</VList>
|
|
491
|
-
</VMenu>
|
|
492
|
-
</VBtnGroup>
|
|
493
649
|
</VCardActions>
|
|
494
650
|
</template>
|
|
495
651
|
</TiptapifyDialog>
|
|
496
652
|
</template>
|
|
497
653
|
|
|
498
654
|
<style lang="scss" scoped>
|
|
499
|
-
.
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
0 1px 5px 0 var(--v-shadow-key-ambient-opacity, rgba(0, 0, 0, 0.12));
|
|
655
|
+
.ai-dialog {
|
|
656
|
+
display: flex;
|
|
657
|
+
flex-direction: column;
|
|
658
|
+
gap: 12px;
|
|
504
659
|
}
|
|
505
660
|
|
|
506
|
-
.ai-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
661
|
+
.ai-context {
|
|
662
|
+
align-items: center;
|
|
663
|
+
color: rgba(var(--v-theme-on-surface), var(--v-medium-emphasis-opacity));
|
|
664
|
+
display: flex;
|
|
665
|
+
font-size: .8125rem;
|
|
666
|
+
gap: 6px;
|
|
667
|
+
min-width: 0;
|
|
668
|
+
|
|
669
|
+
&__label {
|
|
670
|
+
white-space: nowrap;
|
|
671
|
+
}
|
|
510
672
|
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
673
|
+
&__excerpt {
|
|
674
|
+
font-style: italic;
|
|
675
|
+
min-width: 0;
|
|
676
|
+
overflow: hidden;
|
|
677
|
+
text-overflow: ellipsis;
|
|
678
|
+
white-space: nowrap;
|
|
515
679
|
}
|
|
516
680
|
}
|
|
517
681
|
|
|
518
|
-
.ai-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
682
|
+
.ai-examples {
|
|
683
|
+
display: flex;
|
|
684
|
+
flex-wrap: wrap;
|
|
685
|
+
gap: 6px;
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
.ai-prompt {
|
|
689
|
+
border: 1px solid rgba(var(--v-border-color), var(--v-border-opacity));
|
|
690
|
+
border-radius: var(--tf-dialog-field-radius, 8px);
|
|
691
|
+
padding: 6px 8px 6px 12px;
|
|
692
|
+
transition: border-color .15s;
|
|
522
693
|
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
694
|
+
&:focus-within {
|
|
695
|
+
border-color: rgb(var(--v-theme-primary));
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
&--disabled {
|
|
699
|
+
opacity: .6;
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
&__input :deep(.v-field__input) {
|
|
703
|
+
padding-top: 4px;
|
|
704
|
+
padding-bottom: 4px;
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
&__bar {
|
|
708
|
+
align-items: center;
|
|
709
|
+
display: flex;
|
|
710
|
+
gap: 8px;
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
&__hint {
|
|
714
|
+
color: rgba(var(--v-theme-on-surface), var(--v-disabled-opacity));
|
|
715
|
+
font-size: .75rem;
|
|
716
|
+
white-space: nowrap;
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
.ai-thinking {
|
|
721
|
+
color: rgba(var(--v-theme-on-surface), var(--v-medium-emphasis-opacity));
|
|
722
|
+
letter-spacing: normal;
|
|
723
|
+
margin-left: -8px;
|
|
724
|
+
text-transform: none;
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
.ai-status {
|
|
728
|
+
align-items: flex-start;
|
|
729
|
+
display: flex;
|
|
730
|
+
font-size: .8125rem;
|
|
731
|
+
gap: 6px;
|
|
732
|
+
|
|
733
|
+
&--warning {
|
|
734
|
+
color: rgb(var(--v-theme-warning));
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
&--error {
|
|
738
|
+
color: rgb(var(--v-theme-error));
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
.v-icon {
|
|
742
|
+
margin-top: 1px;
|
|
526
743
|
}
|
|
527
744
|
}
|
|
528
745
|
|
|
529
746
|
.ai-reasoning {
|
|
530
|
-
|
|
747
|
+
&__toggle {
|
|
748
|
+
align-items: center;
|
|
749
|
+
background: none;
|
|
750
|
+
border: 0;
|
|
531
751
|
color: rgba(var(--v-theme-on-surface), var(--v-medium-emphasis-opacity));
|
|
752
|
+
cursor: pointer;
|
|
753
|
+
display: inline-flex;
|
|
754
|
+
font: inherit;
|
|
532
755
|
font-size: .8125rem;
|
|
533
|
-
|
|
534
|
-
padding:
|
|
756
|
+
gap: 6px;
|
|
757
|
+
padding: 0;
|
|
535
758
|
}
|
|
536
759
|
|
|
537
|
-
|
|
538
|
-
color: rgba(var(--v-theme-on-surface), var(--v-
|
|
760
|
+
&__text {
|
|
761
|
+
color: rgba(var(--v-theme-on-surface), var(--v-medium-emphasis-opacity));
|
|
762
|
+
font-family: 'JetBrainsMono', monospace;
|
|
763
|
+
font-size: .75rem;
|
|
764
|
+
line-height: 1.4;
|
|
765
|
+
margin: 6px 0 0 22px;
|
|
766
|
+
max-height: 200px;
|
|
767
|
+
overflow: auto;
|
|
768
|
+
white-space: pre-wrap;
|
|
769
|
+
word-break: break-word;
|
|
539
770
|
}
|
|
540
771
|
}
|
|
541
772
|
|
|
542
|
-
.ai-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
}
|
|
773
|
+
.ai-result {
|
|
774
|
+
background: rgba(var(--v-theme-on-surface), .03);
|
|
775
|
+
border: 1px solid rgba(var(--v-border-color), var(--v-border-opacity));
|
|
776
|
+
border-radius: var(--tf-dialog-field-radius, 8px);
|
|
777
|
+
overflow: hidden;
|
|
778
|
+
position: relative;
|
|
779
|
+
|
|
780
|
+
&__progress {
|
|
781
|
+
left: 0;
|
|
782
|
+
position: absolute;
|
|
783
|
+
top: 0;
|
|
784
|
+
}
|
|
554
785
|
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
786
|
+
&__placeholder {
|
|
787
|
+
color: rgba(var(--v-theme-on-surface), var(--v-medium-emphasis-opacity));
|
|
788
|
+
font-size: .875rem;
|
|
789
|
+
padding: 12px;
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
// The preview keeps the whitespace of plain text, the way it is inserted
|
|
793
|
+
&__content {
|
|
794
|
+
font-size: .875rem;
|
|
795
|
+
line-height: 1.55;
|
|
796
|
+
max-height: 280px;
|
|
797
|
+
overflow: auto;
|
|
798
|
+
padding: 10px 12px;
|
|
799
|
+
white-space: pre-wrap;
|
|
800
|
+
word-break: break-word;
|
|
801
|
+
|
|
802
|
+
:deep(> :first-child) {
|
|
803
|
+
margin-top: 0;
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
:deep(> :last-child) {
|
|
807
|
+
margin-bottom: 0;
|
|
808
|
+
}
|
|
559
809
|
|
|
560
|
-
|
|
561
|
-
|
|
810
|
+
:deep(p),
|
|
811
|
+
:deep(ul),
|
|
812
|
+
:deep(ol),
|
|
813
|
+
:deep(blockquote) {
|
|
814
|
+
margin: 0 0 .5em;
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
:deep(ul),
|
|
818
|
+
:deep(ol) {
|
|
819
|
+
padding-left: 1.5em;
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
:deep(img),
|
|
823
|
+
:deep(iframe),
|
|
824
|
+
:deep(video) {
|
|
825
|
+
max-width: 100%;
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
&__footer {
|
|
830
|
+
align-items: center;
|
|
831
|
+
border-top: 1px solid rgba(var(--v-border-color), var(--v-border-opacity));
|
|
832
|
+
display: flex;
|
|
833
|
+
flex-wrap: wrap;
|
|
834
|
+
gap: 4px 12px;
|
|
835
|
+
padding: 2px 4px 2px 12px;
|
|
836
|
+
|
|
837
|
+
.v-btn {
|
|
838
|
+
letter-spacing: normal;
|
|
839
|
+
text-transform: none;
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
&__meta {
|
|
844
|
+
color: rgba(var(--v-theme-on-surface), var(--v-medium-emphasis-opacity));
|
|
845
|
+
font-size: .75rem;
|
|
846
|
+
white-space: nowrap;
|
|
847
|
+
|
|
848
|
+
&--exceeded {
|
|
849
|
+
color: rgb(var(--v-theme-error));
|
|
850
|
+
}
|
|
562
851
|
}
|
|
563
852
|
}
|
|
564
853
|
|
|
565
|
-
.ai-
|
|
566
|
-
|
|
854
|
+
.ai-diff {
|
|
855
|
+
&__insert {
|
|
856
|
+
background: rgba(var(--v-theme-success), .16);
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
&__delete {
|
|
860
|
+
background: rgba(var(--v-theme-error), .12);
|
|
861
|
+
color: rgba(var(--v-theme-on-surface), var(--v-medium-emphasis-opacity));
|
|
862
|
+
text-decoration: line-through;
|
|
863
|
+
}
|
|
567
864
|
}
|
|
568
865
|
</style>
|