diff --git a/app/assets/javascripts/app/controllers/_ui_element/richtext.coffee b/app/assets/javascripts/app/controllers/_ui_element/richtext.coffee
index ff466ae1b7..a7e36bdfe8 100644
--- a/app/assets/javascripts/app/controllers/_ui_element/richtext.coffee
+++ b/app/assets/javascripts/app/controllers/_ui_element/richtext.coffee
@@ -1,5 +1,7 @@
# coffeelint: disable=camel_case_classes
-class App.UiElement.richtext
+class App.UiElement.richtext extends Spine.Module
+ @extend App.TextTools
+
@render: (attributeConfig, params, form) ->
attribute = $.extend(true, {}, attributeConfig)
@@ -108,6 +110,13 @@ class App.UiElement.richtext
uploader.render()
, 100, undefined, 'form_upload')
+ @textToolsInit(
+ item,
+ attribute.disabled,
+ -> item.find('[contenteditable]').trigger('textToolsStart'),
+ -> item.find('[contenteditable]').trigger('textToolsStop'),
+ ) if attribute.text_tools
+
item
@toolButtonClicked: (event, form) ->
diff --git a/app/assets/javascripts/app/controllers/agent_ticket_create.coffee b/app/assets/javascripts/app/controllers/agent_ticket_create.coffee
index ce37f2f189..f026db2d76 100644
--- a/app/assets/javascripts/app/controllers/agent_ticket_create.coffee
+++ b/app/assets/javascripts/app/controllers/agent_ticket_create.coffee
@@ -538,6 +538,8 @@ class App.TicketCreate extends App.Controller
events:
'fileUploadStart .richtext': => @submitDisable()
'fileUploadStop .richtext': => @submitEnable()
+ 'textToolsStart .richtext': => @submitDisable()
+ 'textToolsStop .richtext': => @submitEnable()
'change [name=customer_id]': @localUserInfo
'change [data-attribute-name=organization_id] .js-input': @localUserInfo
richTextUploadRenderCallback: @updateTaskManagerAttachments
diff --git a/app/assets/javascripts/app/controllers/ticket_zoom.coffee b/app/assets/javascripts/app/controllers/ticket_zoom.coffee
index 9e23b28d15..af4cb6660a 100644
--- a/app/assets/javascripts/app/controllers/ticket_zoom.coffee
+++ b/app/assets/javascripts/app/controllers/ticket_zoom.coffee
@@ -532,6 +532,8 @@ class App.TicketZoom extends App.Controller
richTextUploadDeleteCallback: (attachments) =>
@taskUpdateAttachments('article', attachments)
@delay(@markForm, 250, 'ticket-zoom-form-update')
+ richTextTextToolsStartCallback: @submitDisable
+ richTextTextToolsStopCallback: @submitEnable
)
@highlighter = new App.TicketZoomHighlighter(
diff --git a/app/assets/javascripts/app/controllers/ticket_zoom/article_new.coffee b/app/assets/javascripts/app/controllers/ticket_zoom/article_new.coffee
index 569a298206..9242aa67e6 100644
--- a/app/assets/javascripts/app/controllers/ticket_zoom/article_new.coffee
+++ b/app/assets/javascripts/app/controllers/ticket_zoom/article_new.coffee
@@ -508,6 +508,9 @@ class App.TicketZoomArticleNew extends App.Controller
when 'body:allowNoCaption'
@bodyAllowNoCaption = articleType.bodyAllowNoCaption
+ # (Re)initialize text tools on every article type change.
+ App.TextTools.textToolsInit(@textarea.parent(), false, @richTextTextToolsStartCallback, @richTextTextToolsStopCallback)
+
# convert remote src images to data uri
App.Utils.htmlImage2DataUrlAsyncInline(@$('[data-name=body]'))
@@ -630,6 +633,8 @@ class App.TicketZoomArticleNew extends App.Controller
options:
duration: duration
easing: 'easeOutQuad'
+ begin: => @$('.text-tools').hide().css('transform', "translateX(-#{@attachmentInputHolder.position().left}px)")
+ complete: => @$('.text-tools').fadeIn()
@attachmentHint.velocity
properties:
@@ -670,6 +675,7 @@ class App.TicketZoomArticleNew extends App.Controller
options:
duration: 300
easing: 'easeOutQuad'
+ begin: => @$('.text-tools').hide()
@attachmentHint.velocity
properties:
diff --git a/app/assets/javascripts/app/lib/base/jquery.contenteditable.js b/app/assets/javascripts/app/lib/base/jquery.contenteditable.js
index c059b2a87b..2b0434536b 100644
--- a/app/assets/javascripts/app/lib/base/jquery.contenteditable.js
+++ b/app/assets/javascripts/app/lib/base/jquery.contenteditable.js
@@ -326,6 +326,67 @@
})[0]
}
+ Plugin.prototype.getSelection = function() {
+ var result = {
+ content: '',
+ ranges: [],
+ }
+
+ if (window.getSelection || document.getSelection) {
+ var sel
+
+ if (window.getSelection) sel = window.getSelection()
+ else sel = document.getSelection()
+
+ if (sel.rangeCount) {
+ var container = $('
')
+ for (var i = 0; i < sel.rangeCount; ++i) {
+ result.ranges.push(sel.getRangeAt(i))
+ container.append(sel.getRangeAt(i).cloneContents())
+ }
+ if ( this.options.mode === 'textonly' ) {
+ result.content = container.text().trim()
+ }
+ else {
+ result.content = container.html()
+ }
+ }
+ }
+ else if (document.selection) {
+ if (document.selection.type === 'Text') {
+ if ( this.options.mode === 'textonly' ) {
+ result.content = document.selection.createRange().text
+ }
+ else {
+ result.content = document.selection.createRange().htmlText
+ }
+ }
+ }
+
+ return result
+ }
+
+ Plugin.prototype.replaceSelection = function (ranges, content) {
+ if (ranges.length && (window.getSelection || document.getSelection)) {
+ this.log('restore selection')
+
+ var sel
+
+ if (window.getSelection) sel = window.getSelection()
+ else sel = document.getSelection()
+
+ sel.removeAllRanges()
+
+ for (var i = 0; i < ranges.length; i++) {
+ sel.addRange(ranges[i])
+ }
+ }
+
+ content = App.Utils.clipboardHtmlInsertPreperation(content, this.options)
+
+ this.paste(content)
+ }
+
Plugin.prototype.onPaste = function (e) {
e.preventDefault()
var clipboardData, clipboardImage, text, htmlRaw, htmlString
diff --git a/app/assets/javascripts/app/lib/mixins/text_tools.coffee b/app/assets/javascripts/app/lib/mixins/text_tools.coffee
new file mode 100644
index 0000000000..0e20b79810
--- /dev/null
+++ b/app/assets/javascripts/app/lib/mixins/text_tools.coffee
@@ -0,0 +1,96 @@
+# Methods for initializing and using text tools in a richtext editor
+
+App.TextTools =
+ textToolsInit: (el, disabled = false, startCallback = null, stopCallback = null) ->
+ return if not App.User.current()?.permission('ticket.agent')
+ return if not App.Config.get('ai_provider')
+ return if not App.Config.get('ai_assistance_text_tools')
+
+ # Remove any existing text tools, start from scratch.
+ # This is necessary to avoid duplication of text tools in article type switching context.
+ el.find('.text-tools').remove()
+
+ # If attachments are present, append text tools to the same container.
+ if el.find('.article-attachment:not(.hide)').length and not el.find('.article-attachment .text-tools').length
+ el.find('.article-attachment').append( $( App.view('generic/text_tools')(disabled: disabled) ) )
+ el.find('.text-tools').css('transform', el.find('.attachmentPlaceholder').css('transform'))
+
+ # Otherwise, append text tools into their own container.
+ else if not el.find('.text-tools--standalone').length
+ el.append( $( App.view('generic/text_tools')(disabled: disabled, no_attachment: true) ) )
+
+ # Initialize the dropdown menu.
+ # This seems to be necessary only in the article reply context.
+ el.find('[data-toggle="dropdown"]').dropdown()
+
+ # Handle text tool actions.
+ el.off('click.text-tools-actions', '.js-action').on('click.text-tools-actions', '.js-action', (e) ->
+ e.preventDefault()
+ action = $(e.target).data('type')
+ ce = el.find('[contenteditable]').data().plugin_ce
+
+ el.find('[data-toggle="dropdown"]').dropdown('toggle')
+
+ selection = ce?.getSelection()
+
+ if not selection?.content?.length
+ App.Event.trigger('notify', {
+ type: 'info'
+ msg: __('Please select some text first.')
+ timeout: 2000
+ })
+ return
+
+ params =
+ input: selection.content
+ service_type: action
+
+ App.TextTools.textToolsStartLoading(el, startCallback, stopCallback)
+
+ App.Ajax.request(
+ id: 'ai_assistance_text_tools'
+ type: 'POST'
+ url: "#{App.Config.get('api_path')}/ai_assistance/text_tools"
+ data: JSON.stringify(params)
+ processData: true
+ success: (data) ->
+ App.TextTools.textToolsStopLoading(el, stopCallback)
+ ce.replaceSelection(selection.ranges, data.output)
+ error: ->
+ App.TextTools.textToolsStopLoading(el, stopCallback)
+ )
+ )
+
+ textToolsStartLoading: (el, startCallback, stopCallback) ->
+ startCallback?() # callback is used to temporarily disable the submit button
+
+ loader = $( App.view('generic/text_tools_loading')() )
+
+ loader.off('click.text-tools-cancel', '.js-cancel').on('click.text-tools-cancel', '.js-cancel', (e) ->
+ e.preventDefault()
+ App.Ajax.abort('ai_assistance_text_tools')
+ App.TextTools.textToolsStopLoading(el, stopCallback)
+ )
+
+ el.find('[contenteditable]').prop('contenteditable', false)
+
+ if el.find('.article-attachment:not(.hide)').length > 0
+ el.find('.article-attachment').hide()
+ else
+ el.find('.text-tools').hide()
+
+ el.append(loader)
+
+ textToolsStopLoading: (el, stopCallback) ->
+ el.find('.js-loading').remove()
+
+ if el.find('.article-attachment:not(.hide)').length > 0
+ el.find('.article-attachment').show()
+ else
+ el.find('.text-tools').show()
+
+ el.find('[contenteditable]')
+ .prop('contenteditable', true)
+ .focus()
+
+ stopCallback?() # callback is used to re-enable the submit button
diff --git a/app/assets/javascripts/app/views/generic/text_tools.jst.eco b/app/assets/javascripts/app/views/generic/text_tools.jst.eco
new file mode 100644
index 0000000000..14e1ba70b8
--- /dev/null
+++ b/app/assets/javascripts/app/views/generic/text_tools.jst.eco
@@ -0,0 +1,20 @@
+<% if !@disabled: %>
+
+ <%- @Icon('smart-assist-elaborate') %>
+
+
+<% end %>
diff --git a/app/assets/javascripts/app/views/generic/text_tools_loading.jst.eco b/app/assets/javascripts/app/views/generic/text_tools_loading.jst.eco
new file mode 100644
index 0000000000..85cdc9d8d0
--- /dev/null
+++ b/app/assets/javascripts/app/views/generic/text_tools_loading.jst.eco
@@ -0,0 +1,4 @@
+
+ <%- @Icon('smart-assist-elaborate') %><%- @T('Smart Assist is generating text…') %>
+
<%- @T('Cancel') %>
+
diff --git a/app/assets/stylesheets/svg-dimensions.css b/app/assets/stylesheets/svg-dimensions.css
index c2282ecdf0..2790614df2 100644
--- a/app/assets/stylesheets/svg-dimensions.css
+++ b/app/assets/stylesheets/svg-dimensions.css
@@ -144,6 +144,7 @@
.icon-signed { width: 14px; height: 14px; }
.icon-signout { width: 15px; height: 19px; }
.icon-small-dot { width: 16px; height: 16px; }
+.icon-smart-assist-elaborate { width: 20px; height: 20px; }
.icon-smart-assist { width: 16px; height: 16px; }
.icon-sms { width: 17px; height: 17px; }
.icon-spinner-small { width: 15px; height: 15px; }
diff --git a/app/assets/stylesheets/zammad.scss b/app/assets/stylesheets/zammad.scss
index 15ba812b77..2f9307911d 100644
--- a/app/assets/stylesheets/zammad.scss
+++ b/app/assets/stylesheets/zammad.scss
@@ -110,6 +110,8 @@ $mobileNavigationWidthOpen: 220px;
--menu-close-tab-opacity: 0.3;
--menu-border: none;
--menu-border-secondary: none;
+ --ai-assist-blue: hsl(192, 100%, 39%);
+ --ai-assist-pink: hsl(340, 77%, 61%);
}
// dark mode
@@ -2577,16 +2579,27 @@ input.time.time--12 {
padding: 7px 12px 35px;
}
+.textBubble [contenteditable='false'],
.richtext.form-control [contenteditable='false'] {
cursor: not-allowed;
opacity: 0.38;
border-color: var(--border);
- padding: unset;
+ color: var(--text-muted);
+
+ &.enableObjectResizingShim {
+ padding: unset;
+ }
&:focus,
&.focus {
border-color: var(--border-highlight);
}
+
+ &::selection,
+ *::selection {
+ color: var(--text-normal);
+ background: transparent;
+ }
}
.richtext.is-readonly [contenteditable='false'] {
@@ -7261,11 +7274,11 @@ a.list-group-item.active > .badge,
right: 0;
top: 0;
height: 2px;
- background: rgb(0, 158, 198);
+ background: var(--ai-assist-blue);
background: linear-gradient(
90deg,
- rgba(0, 158, 198, 1) 0%,
- rgba(232, 79, 131, 1) 100%
+ var(--ai-assist-blue) 0%,
+ var(--ai-assist-pink) 100%
);
}
}
@@ -7279,7 +7292,7 @@ a.list-group-item.active > .badge,
.icon {
flex-grow: 0;
flex-shrink: 0;
- color: #009ec6;
+ color: var(--ai-assist-blue);
margin-bottom: 1px;
}
@@ -7478,6 +7491,11 @@ a.list-group-item.active > .badge,
margin-bottom: -10px;
}
+.textBubble-footer:has(~ .text-tools) {
+ justify-content: end;
+ gap: 8px;
+}
+
.textBubble-footer:not(.is-open .textBubble-footer) {
visibility: hidden;
}
@@ -7975,9 +7993,81 @@ a.list-group-item.active > .badge,
height: 42px;
padding: 10px 0;
color: var(--text-muted);
- overflow: hidden;
+ display: flex;
+ align-items: center;
+ gap: 16px;
+}
- @extend .u-textTruncate;
+.text-tools {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+
+ &--standalone {
+ position: absolute;
+ bottom: -4px;
+ left: 10px;
+ right: 10px;
+ height: 42px;
+ padding: 10px 0;
+ }
+
+ &::before {
+ display: block;
+ content: '';
+ width: 2px;
+ height: 16px;
+ background: var(--ai-assist-blue);
+ background: linear-gradient(
+ 180deg,
+ var(--ai-assist-blue) 0%,
+ var(--ai-assist-pink) 100%
+ );
+ }
+
+ .icon {
+ width: 16px;
+ height: 16px;
+ fill: var(--interactive-primary);
+ }
+
+ .text-tools-action[data-toggle='dropdown'] {
+ padding: 0;
+ }
+
+ .article-add:not(.is-open) & {
+ display: none;
+ }
+}
+
+.text-tools-loading {
+ position: absolute;
+ bottom: -4px;
+ left: 10px;
+ right: 10px;
+ height: 42px;
+ padding: 10px 0;
+ display: flex;
+ align-items: center;
+ gap: 8px;
+
+ .icon {
+ width: 16px;
+ height: 16px;
+ }
+
+ &::before {
+ display: block;
+ content: '';
+ width: 2px;
+ height: 16px;
+ background: var(--ai-assist-blue);
+ background: linear-gradient(
+ 180deg,
+ var(--ai-assist-blue) 0%,
+ var(--ai-assist-pink) 100%
+ );
+ }
}
.attachments:not(:empty) {
diff --git a/app/controllers/ai_assistance_controller.rb b/app/controllers/ai_assistance_controller.rb
new file mode 100644
index 0000000000..112c615136
--- /dev/null
+++ b/app/controllers/ai_assistance_controller.rb
@@ -0,0 +1,17 @@
+# Copyright (C) 2012-2025 Zammad Foundation, https://zammad-foundation.org/
+
+class AIAssistanceController < ApplicationController
+ prepend_before_action :authenticate_and_authorize!
+
+ def text_tools
+ output = Service::AIAssistance::TextTools.new(
+ input: params[:input],
+ service_type: params[:service_type],
+ current_user:,
+ ).execute
+
+ render json: {
+ output:,
+ }
+ end
+end
diff --git a/app/frontend/apps/desktop/initializer/assets/smart-assist-elaborate.svg b/app/frontend/apps/desktop/initializer/assets/smart-assist-elaborate.svg
new file mode 100644
index 0000000000..0b68384833
--- /dev/null
+++ b/app/frontend/apps/desktop/initializer/assets/smart-assist-elaborate.svg
@@ -0,0 +1,7 @@
+
diff --git a/app/frontend/apps/desktop/initializer/initializeGlobalComponentStyles.ts b/app/frontend/apps/desktop/initializer/initializeGlobalComponentStyles.ts
index 9aa249086d..86667fbf01 100644
--- a/app/frontend/apps/desktop/initializer/initializeGlobalComponentStyles.ts
+++ b/app/frontend/apps/desktop/initializer/initializeGlobalComponentStyles.ts
@@ -1,5 +1,7 @@
// Copyright (C) 2012-2025 Zammad Foundation, https://zammad-foundation.org/
+import { initializeAiAssistantTextTools } from '#shared/components/Form/fields/FieldEditor/AiAssistantTextTools/initializeAiAssistantTextTools.ts'
+import { initializeAiAssistantTextToolsLoadingBanner } from '#shared/components/Form/fields/FieldEditor/AiAssistantTextTools/initializeAiAssistantTextToolsLoadingBanner.ts'
import { initializeEditorColorMenuClasses } from '#shared/components/Form/fields/FieldEditor/FieldEditorColorMenu/initializeEditorColorMenu.ts'
import { initializeAlertClasses } from '#shared/initializer/initializeAlertClasses.ts'
import { initializeAvatarClasses } from '#shared/initializer/initializeAvatarClasses.ts'
@@ -85,6 +87,15 @@ export const initializeGlobalComponentStyles = () => {
},
})
+ initializeAiAssistantTextTools({
+ popover: {
+ base: 'min-w-[13.5rem] rounded-xl overflow-hidden',
+ button:
+ 'text-sm outline-none p-3 text-left active:text-white active:bg-blue-800 dark:active:bg-blue-800 dark:hover:text-white hover:text-black inline-block w-full dark:text-neutral-400 focus-visible:bg-blue-800 focus-visible:text-white hover:bg-blue-600 dark:hover:bg-blue-900 text-gray-100',
+ },
+ verticalGradient: 'bg-linear-to-t from-pink-200 to-blue-800',
+ })
+
initializeFilePreviewClasses({
base: 'dark:text-white text-black text-sm leading-snug',
wrapper: 'p-2.5',
@@ -94,4 +105,10 @@ export const initializeGlobalComponentStyles = () => {
size: 'dark:text-neutral-500 text-stone-400 text-xs leading-snug',
icon: 'dark:text-neutral-500 text-stone-400',
})
+
+ initializeAiAssistantTextToolsLoadingBanner({
+ icon: 'text-blue-800',
+ label: 'text-black! dark:text-white!',
+ button: 'text-blue-800',
+ })
}
diff --git a/app/frontend/apps/mobile/initializer/assets/smart-assist-elaborate.svg b/app/frontend/apps/mobile/initializer/assets/smart-assist-elaborate.svg
new file mode 100644
index 0000000000..67f14591b0
--- /dev/null
+++ b/app/frontend/apps/mobile/initializer/assets/smart-assist-elaborate.svg
@@ -0,0 +1,7 @@
+
diff --git a/app/frontend/apps/mobile/initializer/assets/smart-assist.svg b/app/frontend/apps/mobile/initializer/assets/smart-assist.svg
new file mode 100644
index 0000000000..3a70f7cf4d
--- /dev/null
+++ b/app/frontend/apps/mobile/initializer/assets/smart-assist.svg
@@ -0,0 +1,7 @@
+
diff --git a/app/frontend/apps/mobile/initializer/initializeGlobalComponentStyles.ts b/app/frontend/apps/mobile/initializer/initializeGlobalComponentStyles.ts
index 2ecae109f3..d8b1f04ad9 100644
--- a/app/frontend/apps/mobile/initializer/initializeGlobalComponentStyles.ts
+++ b/app/frontend/apps/mobile/initializer/initializeGlobalComponentStyles.ts
@@ -1,5 +1,7 @@
// Copyright (C) 2012-2025 Zammad Foundation, https://zammad-foundation.org/
+import { initializeAiAssistantTextTools } from '#shared/components/Form/fields/FieldEditor/AiAssistantTextTools/initializeAiAssistantTextTools.ts'
+import { initializeAiAssistantTextToolsLoadingBanner } from '#shared/components/Form/fields/FieldEditor/AiAssistantTextTools/initializeAiAssistantTextToolsLoadingBanner.ts'
import { initializeEditorColorMenuClasses } from '#shared/components/Form/fields/FieldEditor/FieldEditorColorMenu/initializeEditorColorMenu.ts'
import { initializeAlertClasses } from '#shared/initializer/initializeAlertClasses.ts'
import { initializeAvatarClasses } from '#shared/initializer/initializeAvatarClasses.ts'
@@ -57,7 +59,7 @@ export const initializeGlobalComponentStyles = () => {
})
initializePopoverClasses({
- base: 'min-h-9 rounded-xl max-w-[calc(100vw-8px)] text-white top-0 border border-gray-500 bg-gray-400 antialiased rtl:right-1/2 ltr:left-1/2 rtl:translate-x-1/2 ltr:-translate-x-1/2',
+ base: 'min-h-9 rounded-xl max-w-[calc(100vw-8px)] text-white top-0 border border-gray-500 bg-gray-400 antialiased',
arrow: 'hidden',
})
@@ -80,4 +82,18 @@ export const initializeGlobalComponentStyles = () => {
size: 'text-white/80',
icon: 'border-gray-300',
})
+
+ initializeAiAssistantTextTools({
+ popover: {
+ base: '',
+ button: 'py-4 px-3',
+ },
+ verticalGradient: 'bg-linear-to-t from-pink to-blue',
+ })
+
+ initializeAiAssistantTextToolsLoadingBanner({
+ icon: 'text-blue',
+ label: 'text-white',
+ button: 'text-blue',
+ })
}
diff --git a/app/frontend/apps/mobile/styles/main.css b/app/frontend/apps/mobile/styles/main.css
index 9b095faf12..c598aca7f3 100644
--- a/app/frontend/apps/mobile/styles/main.css
+++ b/app/frontend/apps/mobile/styles/main.css
@@ -199,3 +199,16 @@
@utility required {
@apply after:bg-yellow after:ms-1 after:inline-block after:h-1 after:w-1 after:rounded-full after:align-middle after:content-[''];
}
+
+@utility ai-stripe {
+ &::before {
+ content: '';
+ height: 2px;
+ width: 100%;
+ background-image: repeating-linear-gradient(
+ to right,
+ var(--color-blue),
+ var(--color-pink)
+ );
+ }
+}
diff --git a/app/frontend/shared/components/CommonPopover/CommonPopover.vue b/app/frontend/shared/components/CommonPopover/CommonPopover.vue
index bb648b9cce..ddfb26d21c 100644
--- a/app/frontend/shared/components/CommonPopover/CommonPopover.vue
+++ b/app/frontend/shared/components/CommonPopover/CommonPopover.vue
@@ -19,6 +19,7 @@ import {
useTemplateRef,
} from 'vue'
+import { useAppName } from '#shared/composables/useAppName.ts'
import { useTransitionConfig } from '#shared/composables/useTransitionConfig.ts'
import { useTrapTab } from '#shared/composables/useTrapTab.ts'
import { EnumTextDirection } from '#shared/graphql/types.ts'
@@ -126,8 +127,17 @@ const PLACEMENT_OFFSET_WITH_ARROW = 30
const ORIENTATION_OFFSET_WO_ARROW = 6
const ORIENTATION_OFFSET_WITH_ARROW = 16
+const appName = useAppName()
// eslint-disable-next-line sonarjs/cognitive-complexity
const popoverStyle = computed(() => {
+ if (appName === 'mobile') {
+ return {
+ top: '50%',
+ left: '50%',
+ transform: 'translate(-50%, -50%)',
+ }
+ }
+
if (!targetElementBounds.value) return { top: 0, left: 0, maxHeight: 0 }
const maxHeight = hasDirectionUp.value
diff --git a/app/frontend/shared/components/Form/fields/FieldEditor/ActionBar.vue b/app/frontend/shared/components/Form/fields/FieldEditor/ActionBar.vue
index d03605b0f5..88633c5078 100644
--- a/app/frontend/shared/components/Form/fields/FieldEditor/ActionBar.vue
+++ b/app/frontend/shared/components/Form/fields/FieldEditor/ActionBar.vue
@@ -197,7 +197,10 @@ const leftgradientvalue = computed(() => classes.actionBar.leftGradient.left)
/>
-
+
diff --git a/app/frontend/shared/components/Form/fields/FieldEditor/AiAssistantTextTools/AiAssistantLoadingBanner.vue b/app/frontend/shared/components/Form/fields/FieldEditor/AiAssistantTextTools/AiAssistantLoadingBanner.vue
new file mode 100644
index 0000000000..e3fc12ed5e
--- /dev/null
+++ b/app/frontend/shared/components/Form/fields/FieldEditor/AiAssistantTextTools/AiAssistantLoadingBanner.vue
@@ -0,0 +1,54 @@
+
+
+
+
+
+
+
+
+
+ {{ $t('Generating text…') }}
+
+
+
+
diff --git a/app/frontend/shared/components/Form/fields/FieldEditor/AiAssistantTextTools/AiAssistantTextTools.vue b/app/frontend/shared/components/Form/fields/FieldEditor/AiAssistantTextTools/AiAssistantTextTools.vue
new file mode 100644
index 0000000000..1b9519b568
--- /dev/null
+++ b/app/frontend/shared/components/Form/fields/FieldEditor/AiAssistantTextTools/AiAssistantTextTools.vue
@@ -0,0 +1,229 @@
+
+
+
+
+
+
+
+ -
+
+
+
+
+
+
+
diff --git a/app/frontend/shared/components/Form/fields/FieldEditor/AiAssistantTextTools/initializeAiAssistantTextTools.ts b/app/frontend/shared/components/Form/fields/FieldEditor/AiAssistantTextTools/initializeAiAssistantTextTools.ts
new file mode 100644
index 0000000000..7ac1485e78
--- /dev/null
+++ b/app/frontend/shared/components/Form/fields/FieldEditor/AiAssistantTextTools/initializeAiAssistantTextTools.ts
@@ -0,0 +1,17 @@
+// Copyright (C) 2012-2025 Zammad Foundation, https://zammad-foundation.org/
+
+let aiAssistantTextTools = {
+ popover: {
+ base: '',
+ button: '',
+ },
+ verticalGradient: '',
+}
+
+export const initializeAiAssistantTextTools = (
+ classes: typeof aiAssistantTextTools,
+) => {
+ aiAssistantTextTools = classes
+}
+
+export const getAiAssistantTextToolsClasses = () => aiAssistantTextTools
diff --git a/app/frontend/shared/components/Form/fields/FieldEditor/AiAssistantTextTools/initializeAiAssistantTextToolsLoadingBanner.ts b/app/frontend/shared/components/Form/fields/FieldEditor/AiAssistantTextTools/initializeAiAssistantTextToolsLoadingBanner.ts
new file mode 100644
index 0000000000..3d477e43b8
--- /dev/null
+++ b/app/frontend/shared/components/Form/fields/FieldEditor/AiAssistantTextTools/initializeAiAssistantTextToolsLoadingBanner.ts
@@ -0,0 +1,16 @@
+// Copyright (C) 2012-2025 Zammad Foundation, https://zammad-foundation.org/
+
+let aiAssistantTextToolsLoadingBanner = {
+ icon: '',
+ label: '',
+ button: '',
+}
+
+export const initializeAiAssistantTextToolsLoadingBanner = (
+ classes: typeof aiAssistantTextToolsLoadingBanner,
+) => {
+ aiAssistantTextToolsLoadingBanner = classes
+}
+
+export const getAiAssistantTextToolsLoadingBannerClasses = () =>
+ aiAssistantTextToolsLoadingBanner
diff --git a/app/frontend/shared/components/Form/fields/FieldEditor/FieldEditorActionBar.vue b/app/frontend/shared/components/Form/fields/FieldEditor/FieldEditorActionBar.vue
index 6352388a9b..7c5ab63260 100644
--- a/app/frontend/shared/components/Form/fields/FieldEditor/FieldEditorActionBar.vue
+++ b/app/frontend/shared/components/Form/fields/FieldEditor/FieldEditorActionBar.vue
@@ -1,16 +1,23 @@
-
-
-
-
-
-
-
+
-
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/frontend/shared/components/Form/fields/FieldEditor/FieldEditorInput.vue b/app/frontend/shared/components/Form/fields/FieldEditor/FieldEditorInput.vue
index 53ad74579a..117c504ad9 100644
--- a/app/frontend/shared/components/Form/fields/FieldEditor/FieldEditorInput.vue
+++ b/app/frontend/shared/components/Form/fields/FieldEditor/FieldEditorInput.vue
@@ -429,7 +429,7 @@ const classes = getFieldEditorClasses()
:content-type="contentType"
:visible="showActionBar"
:disabled-plugins="disabledPlugins"
- :form-id="context.formId"
+ :form-context="reactiveContext"
@hide="showActionBar = false"
@blur="focusEditor"
/>
diff --git a/app/frontend/shared/components/Form/fields/FieldEditor/__tests__/FieldEditorActionBar.spec.ts b/app/frontend/shared/components/Form/fields/FieldEditor/__tests__/FieldEditorActionBar.spec.ts
index 388e4ce65a..3bb2f59ab7 100644
--- a/app/frontend/shared/components/Form/fields/FieldEditor/__tests__/FieldEditorActionBar.spec.ts
+++ b/app/frontend/shared/components/Form/fields/FieldEditor/__tests__/FieldEditorActionBar.spec.ts
@@ -1,7 +1,16 @@
// Copyright (C) 2012-2025 Zammad Foundation, https://zammad-foundation.org/
-import { renderComponent } from '#tests/support/components/index.ts'
+import { within } from '@testing-library/vue'
+import { renderComponent } from '#tests/support/components/index.ts'
+import { mockApplicationConfig } from '#tests/support/mock-applicationConfig.ts'
+import { mockPermissions } from '#tests/support/mock-permissions.ts'
+
+import {
+ mockAiAssistanceTextToolsMutation,
+ waitForAiAssistanceTextToolsMutationCalls,
+} from '#shared/graphql/mutations/aiAssistanceTextTools.mocks.ts'
+import { EnumAiTextToolService } from '#shared/graphql/types.ts'
import getUuid from '#shared/utils/getUuid.ts'
import FieldEditorActionBar from '../FieldEditorActionBar.vue'
@@ -19,6 +28,20 @@ vi.mock('@tiptap/pm/state', () => {
}
})
+vi.mock('prosemirror-model', () => {
+ return {
+ DOMSerializer: {
+ fromSchema: vi.fn(() => ({
+ serializeFragment: vi.fn(() => {
+ const fragment = document.createDocumentFragment()
+ fragment.textContent = 'selected text'
+ return fragment
+ }),
+ })),
+ },
+ }
+})
+
describe('keyboard interactions', () => {
it('can use arrows to traverse toolbar', async () => {
const view = renderComponent(FieldEditorActionBar, {
@@ -173,4 +196,171 @@ describe('basic toolbar testing', () => {
view.queryByLabelText('Format as underlined'),
).not.toBeInTheDocument()
})
+
+ describe('AiAssistantTextTools', () => {
+ const createMockEditor = () => ({
+ state: {
+ selection: {
+ from: 0,
+ to: 10,
+ anchor: 0,
+ head: 10,
+ empty: false,
+ content: () => 'selected text',
+ },
+ doc: {
+ textBetween: vi.fn(() => 'selected text'),
+ },
+ },
+ chain: vi.fn(() => ({
+ focus: vi.fn(() => ({
+ setTextSelection: vi.fn(() => ({
+ run: vi.fn(),
+ })),
+ })),
+ })),
+ isActive: vi.fn(() => true),
+ getAttributes: vi.fn(() => ({})),
+ commands: {
+ deleteSelection: vi.fn(),
+ insertContentAt: vi.fn(),
+ focus: vi.fn(),
+ },
+ setEditable: vi.fn(),
+ on: vi.fn(),
+ emit: vi.fn(),
+ })
+
+ it('hides feature if flag is not set', async () => {
+ mockApplicationConfig({
+ ai_assistance_text_tools: false,
+ ai_provider: 'openai',
+ })
+
+ mockPermissions(['ticket.agent'])
+
+ const wrapper = renderComponent(FieldEditorActionBar, {
+ props: {
+ contentType: 'text/plain',
+ visible: true,
+ disabledPlugins: [],
+ formId: getUuid(),
+ },
+ })
+
+ expect(
+ wrapper.queryByRole('button', { name: 'Ai assistant text tools' }),
+ ).not.toBeInTheDocument()
+ })
+
+ it('hides the feature if user is customer', async () => {
+ mockApplicationConfig({
+ ai_assistance_text_tools: true,
+ ai_provider: 'openai',
+ })
+
+ mockPermissions(['ticket.customer'])
+
+ const wrapper = renderComponent(FieldEditorActionBar, {
+ props: {
+ contentType: 'text/plain',
+ visible: true,
+ disabledPlugins: [],
+ formId: getUuid(),
+ },
+ })
+
+ expect(
+ wrapper.queryByRole('button', { name: 'Ai assistant text tools' }),
+ ).not.toBeInTheDocument()
+ })
+
+ it('hides the feature if user ai provider is not set', async () => {
+ mockApplicationConfig({
+ ai_assistance_text_tools: true,
+ ai_provider: undefined,
+ })
+
+ mockPermissions(['ticket.customer'])
+
+ const wrapper = renderComponent(FieldEditorActionBar, {
+ props: {
+ contentType: 'text/plain',
+ visible: true,
+ disabledPlugins: [],
+ formId: getUuid(),
+ },
+ })
+
+ expect(
+ wrapper.queryByRole('button', { name: 'Ai assistant text tools' }),
+ ).not.toBeInTheDocument()
+ })
+
+ it.each([
+ {
+ label: 'Improve writing',
+ aiTextToolService: EnumAiTextToolService.ImproveWriting,
+ },
+ {
+ label: 'Fix spelling and grammar',
+ aiTextToolService: EnumAiTextToolService.SpellingAndGrammar,
+ },
+ {
+ label: 'Expand',
+ aiTextToolService: EnumAiTextToolService.Expand,
+ },
+ {
+ label: 'Simplify',
+ aiTextToolService: EnumAiTextToolService.Simplify,
+ },
+ ])('can use $label action', async ({ aiTextToolService, label }) => {
+ mockApplicationConfig({
+ ai_assistance_text_tools: true,
+ ai_provider: 'openai',
+ })
+
+ mockPermissions(['ticket.agent'])
+
+ mockAiAssistanceTextToolsMutation({
+ aiAssistanceTextTools: {
+ output: 'selected text',
+ },
+ })
+ const mockEditor = createMockEditor()
+
+ const wrapper = renderComponent(FieldEditorActionBar, {
+ props: {
+ contentType: 'text/plain',
+ visible: true,
+ disabledPlugins: [],
+ formId: getUuid(),
+ editor: mockEditor,
+ },
+ })
+
+ await wrapper.events.click(
+ wrapper.getByRole('button', { name: 'Ai assistant text tools' }),
+ )
+
+ const popover = await wrapper.findByRole('region', {
+ name: 'Ai assistant text tools',
+ })
+
+ await wrapper.events.click(
+ within(popover).getByRole('button', { name: label }),
+ )
+
+ expect(mockEditor.setEditable).toHaveBeenCalledWith(false)
+
+ const calls = await waitForAiAssistanceTextToolsMutationCalls()
+
+ expect(calls.at(-1)?.variables).toEqual({
+ input: 'selected text',
+ serviceType: aiTextToolService,
+ })
+
+ expect(mockEditor.setEditable).toHaveBeenCalledWith(true)
+ })
+ })
})
diff --git a/app/frontend/shared/components/Form/fields/FieldEditor/types.ts b/app/frontend/shared/components/Form/fields/FieldEditor/types.ts
index 2dc6e532e0..7d1e2cbd4b 100644
--- a/app/frontend/shared/components/Form/fields/FieldEditor/types.ts
+++ b/app/frontend/shared/components/Form/fields/FieldEditor/types.ts
@@ -106,3 +106,9 @@ export interface FieldEditorProps {
}
export type EditorCustomPlugins = keyof ConfidentTake
+
+declare module '@tiptap/vue-3' {
+ interface EditorEvents {
+ 'cancel-ai-assistant-text-tools-updates': void
+ }
+}
diff --git a/app/frontend/shared/components/Form/fields/FieldEditor/useEditorActions.ts b/app/frontend/shared/components/Form/fields/FieldEditor/useEditorActions.ts
index 28f0d92775..9454a41b4a 100644
--- a/app/frontend/shared/components/Form/fields/FieldEditor/useEditorActions.ts
+++ b/app/frontend/shared/components/Form/fields/FieldEditor/useEditorActions.ts
@@ -1,9 +1,15 @@
// Copyright (C) 2012-2025 Zammad Foundation, https://zammad-foundation.org/
+import { storeToRefs } from 'pinia'
import { computed, nextTick, onUnmounted } from 'vue'
+import AiAssistantTextTools from '#shared/components/Form/fields/FieldEditor/AiAssistantTextTools/AiAssistantTextTools.vue'
+import { getAiAssistantTextToolsClasses } from '#shared/components/Form/fields/FieldEditor/AiAssistantTextTools/initializeAiAssistantTextTools.ts'
import { i18n } from '#shared/i18n.ts'
+import { useApplicationStore } from '#shared/stores/application.ts'
import { useLocaleStore } from '#shared/stores/locale.ts'
+import { useSessionStore } from '#shared/stores/session.ts'
+import type { ConfigList } from '#shared/types/config.ts'
import getUuid from '#shared/utils/getUuid.ts'
import testFlags from '#shared/utils/testFlags.ts'
@@ -37,6 +43,9 @@ export interface EditorButton {
command?: (e: MouseEvent) => void
disabled?: boolean
showDivider?: boolean
+ dividerClass?: string
+ permission?: string
+ show?: (config: ConfigList) => boolean
subMenu?: Component | Except[]
}
@@ -87,10 +96,28 @@ export default function useEditorActions(
fileInput = null
})
+ const { config: applicationConfig } = storeToRefs(useApplicationStore())
+ const { hasPermission } = useSessionStore()
+
const { localeData } = useLocaleStore()
+
+ const { verticalGradient } = getAiAssistantTextToolsClasses()
// eslint-disable-next-line sonarjs/cognitive-complexity
const getActionsList = (): EditorButton[] => {
return [
+ {
+ id: getUuid(),
+ name: 'aiAssistantTextTools',
+ contentType: ['text/html', 'text/plain'],
+ label: i18n.t('Ai assistant text tools'),
+ showDivider: true,
+ dividerClass: verticalGradient,
+ permission: 'ticket.agent',
+ show: (config) =>
+ config.ai_assistance_text_tools && !!config.ai_provider,
+ icon: 'smart-assist-elaborate',
+ subMenu: AiAssistantTextTools,
+ },
{
id: `action-${getUuid()}`,
name: 'bold',
@@ -356,14 +383,17 @@ export default function useEditorActions(
]
}
- const actions = computed(() => {
- return getActionsList().filter((action) => {
- if (disabledPlugins.includes(action.name)) {
- return false
- }
+ const actions = computed(() =>
+ getActionsList().filter((action) => {
+ if (disabledPlugins.includes(action.name)) return false
+
+ if (action.show && !action.show(applicationConfig.value)) return false
+
+ if (action.permission && !hasPermission(action.permission)) return false
+
return action.contentType.includes(contentType)
- })
- })
+ }),
+ )
return {
focused,
diff --git a/app/frontend/shared/graphql/mutations/aiAssistanceTextTools.api.ts b/app/frontend/shared/graphql/mutations/aiAssistanceTextTools.api.ts
new file mode 100644
index 0000000000..d36012f534
--- /dev/null
+++ b/app/frontend/shared/graphql/mutations/aiAssistanceTextTools.api.ts
@@ -0,0 +1,18 @@
+import * as Types from '#shared/graphql/types.ts';
+
+import gql from 'graphql-tag';
+import * as VueApolloComposable from '@vue/apollo-composable';
+import * as VueCompositionApi from 'vue';
+export type ReactiveFunction = () => TParam;
+
+export const AiAssistanceTextToolsDocument = gql`
+ mutation aiAssistanceTextTools($input: String!, $serviceType: EnumAITextToolService!) {
+ aiAssistanceTextTools(input: $input, serviceType: $serviceType) {
+ output
+ }
+}
+ `;
+export function useAiAssistanceTextToolsMutation(options: VueApolloComposable.UseMutationOptions | ReactiveFunction> = {}) {
+ return VueApolloComposable.useMutation(AiAssistanceTextToolsDocument, options);
+}
+export type AiAssistanceTextToolsMutationCompositionFunctionResult = VueApolloComposable.UseMutationReturn;
\ No newline at end of file
diff --git a/app/frontend/shared/graphql/mutations/aiAssistanceTextTools.graphql b/app/frontend/shared/graphql/mutations/aiAssistanceTextTools.graphql
new file mode 100644
index 0000000000..4bea8e35ea
--- /dev/null
+++ b/app/frontend/shared/graphql/mutations/aiAssistanceTextTools.graphql
@@ -0,0 +1,5 @@
+mutation aiAssistanceTextTools($input: String!, $serviceType: EnumAITextToolService!) {
+ aiAssistanceTextTools(input: $input, serviceType: $serviceType) {
+ output
+ }
+}
diff --git a/app/frontend/shared/graphql/mutations/aiAssistanceTextTools.mocks.ts b/app/frontend/shared/graphql/mutations/aiAssistanceTextTools.mocks.ts
new file mode 100644
index 0000000000..9dd99bc0d5
--- /dev/null
+++ b/app/frontend/shared/graphql/mutations/aiAssistanceTextTools.mocks.ts
@@ -0,0 +1,17 @@
+import * as Types from '#shared/graphql/types.ts';
+
+import * as Mocks from '#tests/graphql/builders/mocks.ts'
+import * as Operations from './aiAssistanceTextTools.api.ts'
+import * as ErrorTypes from '#shared/types/error.ts'
+
+export function mockAiAssistanceTextToolsMutation(defaults: Mocks.MockDefaultsValue) {
+ return Mocks.mockGraphQLResult(Operations.AiAssistanceTextToolsDocument, defaults)
+}
+
+export function waitForAiAssistanceTextToolsMutationCalls() {
+ return Mocks.waitForGraphQLMockCalls(Operations.AiAssistanceTextToolsDocument)
+}
+
+export function mockAiAssistanceTextToolsMutationError(message: string, extensions: {type: ErrorTypes.GraphQLErrorTypes }) {
+ return Mocks.mockGraphQLResultWithError(Operations.AiAssistanceTextToolsDocument, message, extensions);
+}
diff --git a/app/frontend/shared/graphql/types.ts b/app/frontend/shared/graphql/types.ts
index 3754c0fda4..d6368a4d1f 100644
--- a/app/frontend/shared/graphql/types.ts
+++ b/app/frontend/shared/graphql/types.ts
@@ -21,6 +21,15 @@ export type Scalars = {
UriHttpString: { input: string; output: string; }
};
+/** Autogenerated return type of AIAssistanceTextTools. */
+export type AiAssistanceTextToolsPayload = {
+ __typename?: 'AIAssistanceTextToolsPayload';
+ /** Errors encountered during execution of the mutation. */
+ errors?: Maybe>;
+ /** Returned text */
+ output?: Maybe;
+};
+
/** Objects used to build activity message */
export type ActivityMessageMetaObject = DataPrivacyTask | Group | Organization | Role | Ticket | TicketArticle | User;
@@ -622,6 +631,14 @@ export type EmailAddressParsed = {
name?: Maybe;
};
+/** Available AI text tool services */
+export enum EnumAiTextToolService {
+ Expand = 'expand',
+ ImproveWriting = 'improve_writing',
+ Simplify = 'simplify',
+ SpellingAndGrammar = 'spelling_and_grammar'
+}
+
/** Possible AfterAuth message types */
export enum EnumAfterAuthType {
/** TwoFactorConfiguration */
@@ -1482,6 +1499,8 @@ export type Mutations = {
adminPasswordAuthSend?: Maybe;
/** Verify admin password authentication */
adminPasswordAuthVerify?: Maybe;
+ /** Run an AI text tool service on the supplied text or HTML content */
+ aiAssistanceTextTools?: Maybe;
/** Create a new email channel. This does not perform email validation. */
channelEmailAdd?: Maybe;
/** Try to guess email channel configuration from user credentials */
@@ -1695,6 +1714,13 @@ export type MutationsAdminPasswordAuthVerifyArgs = {
};
+/** All available mutations */
+export type MutationsAiAssistanceTextToolsArgs = {
+ input: Scalars['String']['input'];
+ serviceType: EnumAiTextToolService;
+};
+
+
/** All available mutations */
export type MutationsChannelEmailAddArgs = {
input: ChannelEmailAddInput;
@@ -7089,6 +7115,14 @@ export type UserDetailAttributesFragment = { __typename?: 'User', id: string, in
export type UserPersonalSettingsFragment = { __typename?: 'User', personalSettings?: { __typename?: 'UserPersonalSettings', notificationConfig?: { __typename?: 'UserPersonalSettingsNotificationConfig', groupIds?: Array | null, matrix?: { __typename?: 'UserPersonalSettingsNotificationMatrix', create?: { __typename?: 'UserPersonalSettingsNotificationMatrixRow', channel?: { __typename?: 'UserPersonalSettingsNotificationMatrixChannel', email?: boolean | null, online?: boolean | null } | null, criteria?: { __typename?: 'UserPersonalSettingsNotificationMatrixCriteria', no?: boolean | null, ownedByMe?: boolean | null, ownedByNobody?: boolean | null, subscribed?: boolean | null } | null } | null, escalation?: { __typename?: 'UserPersonalSettingsNotificationMatrixRow', channel?: { __typename?: 'UserPersonalSettingsNotificationMatrixChannel', email?: boolean | null, online?: boolean | null } | null, criteria?: { __typename?: 'UserPersonalSettingsNotificationMatrixCriteria', no?: boolean | null, ownedByMe?: boolean | null, ownedByNobody?: boolean | null, subscribed?: boolean | null } | null } | null, reminderReached?: { __typename?: 'UserPersonalSettingsNotificationMatrixRow', channel?: { __typename?: 'UserPersonalSettingsNotificationMatrixChannel', email?: boolean | null, online?: boolean | null } | null, criteria?: { __typename?: 'UserPersonalSettingsNotificationMatrixCriteria', no?: boolean | null, ownedByMe?: boolean | null, ownedByNobody?: boolean | null, subscribed?: boolean | null } | null } | null, update?: { __typename?: 'UserPersonalSettingsNotificationMatrixRow', channel?: { __typename?: 'UserPersonalSettingsNotificationMatrixChannel', email?: boolean | null, online?: boolean | null } | null, criteria?: { __typename?: 'UserPersonalSettingsNotificationMatrixCriteria', no?: boolean | null, ownedByMe?: boolean | null, ownedByNobody?: boolean | null, subscribed?: boolean | null } | null } | null } | null } | null, notificationSound?: { __typename?: 'UserPersonalSettingsNotificationSound', enabled?: boolean | null, file?: EnumNotificationSoundFile | null } | null } | null };
+export type AiAssistanceTextToolsMutationVariables = Exact<{
+ input: Scalars['String']['input'];
+ serviceType: EnumAiTextToolService;
+}>;
+
+
+export type AiAssistanceTextToolsMutation = { __typename?: 'Mutations', aiAssistanceTextTools?: { __typename?: 'AIAssistanceTextToolsPayload', output?: string | null } | null };
+
export type LoginMutationVariables = Exact<{
input: LoginInput;
}>;
diff --git a/app/frontend/shared/types/config.ts b/app/frontend/shared/types/config.ts
index 40d8b16d1c..71444f01cf 100644
--- a/app/frontend/shared/types/config.ts
+++ b/app/frontend/shared/types/config.ts
@@ -3,6 +3,7 @@ export interface ConfigList {
'active_storage.web_image_content_types': string[]
'auth_saml_credentials.display_name'?: string
'auth_openid_connect_credentials.display_name'?: string
+ ai_assistance_text_tools: boolean
ai_assistance_ticket_summary: boolean
ai_assistance_ticket_summary_config: unknown
ai_provider: string
diff --git a/app/graphql/gql/mutations/ai_assistance/text_tools.rb b/app/graphql/gql/mutations/ai_assistance/text_tools.rb
new file mode 100644
index 0000000000..0ba02da7ce
--- /dev/null
+++ b/app/graphql/gql/mutations/ai_assistance/text_tools.rb
@@ -0,0 +1,24 @@
+# Copyright (C) 2012-2025 Zammad Foundation, https://zammad-foundation.org/
+
+module Gql::Mutations
+ class AIAssistance::TextTools < BaseMutation
+ description 'Run an AI text tool service on the supplied text or HTML content'
+
+ argument :input, String, description: 'Text or HTML content to run the text tool service on'
+ argument :service_type, Gql::Types::Enum::AITextToolServiceType, description: 'The text tool service to use'
+
+ field :output, String, description: 'Returned text'
+
+ def resolve(input:, service_type:)
+ output = Service::AIAssistance::TextTools.new(
+ input:,
+ service_type:,
+ current_user: context.current_user,
+ ).execute
+
+ {
+ output:,
+ }
+ end
+ end
+end
diff --git a/app/graphql/gql/types/enum/ai_text_tool_service_type.rb b/app/graphql/gql/types/enum/ai_text_tool_service_type.rb
new file mode 100644
index 0000000000..1e8249a3d6
--- /dev/null
+++ b/app/graphql/gql/types/enum/ai_text_tool_service_type.rb
@@ -0,0 +1,15 @@
+# Copyright (C) 2012-2025 Zammad Foundation, https://zammad-foundation.org/
+
+module Gql::Types::Enum
+ class AITextToolServiceType < BaseEnum
+ description 'Available AI text tool services'
+
+ build_string_list_enum(
+ AI::Service.list
+ .map { |klass| klass.name.demodulize.underscore }
+ .select { |s| s.start_with?('text_') }
+ .map { |s| s.delete_prefix('text_') }
+ .sort
+ )
+ end
+end
diff --git a/app/graphql/graphql_introspection.json b/app/graphql/graphql_introspection.json
index 273865bd38..5b310b076d 100644
--- a/app/graphql/graphql_introspection.json
+++ b/app/graphql/graphql_introspection.json
@@ -11,6 +11,49 @@
"name": "Subscriptions"
},
"types": [
+ {
+ "kind": "OBJECT",
+ "name": "AIAssistanceTextToolsPayload",
+ "description": "Autogenerated return type of AIAssistanceTextTools.",
+ "fields": [
+ {
+ "name": "errors",
+ "description": "Errors encountered during execution of the mutation.",
+ "args": [],
+ "type": {
+ "kind": "LIST",
+ "name": null,
+ "ofType": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "OBJECT",
+ "name": "UserError",
+ "ofType": null
+ }
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "output",
+ "description": "Returned text",
+ "args": [],
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ }
+ ],
+ "inputFields": null,
+ "interfaces": [],
+ "enumValues": null,
+ "possibleTypes": null
+ },
{
"kind": "UNION",
"name": "ActivityMessageMetaObject",
@@ -4031,6 +4074,41 @@
"enumValues": null,
"possibleTypes": null
},
+ {
+ "kind": "ENUM",
+ "name": "EnumAITextToolService",
+ "description": "Available AI text tool services",
+ "fields": null,
+ "inputFields": null,
+ "interfaces": null,
+ "enumValues": [
+ {
+ "name": "expand",
+ "description": null,
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "improve_writing",
+ "description": null,
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "simplify",
+ "description": null,
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "spelling_and_grammar",
+ "description": null,
+ "isDeprecated": false,
+ "deprecationReason": null
+ }
+ ],
+ "possibleTypes": null
+ },
{
"kind": "ENUM",
"name": "EnumAfterAuthType",
@@ -8889,6 +8967,47 @@
"isDeprecated": false,
"deprecationReason": null
},
+ {
+ "name": "aiAssistanceTextTools",
+ "description": "Run an AI text tool service on the supplied text or HTML content",
+ "args": [
+ {
+ "name": "input",
+ "description": "Text or HTML content to run the text tool service on",
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ }
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "serviceType",
+ "description": "The text tool service to use",
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "ENUM",
+ "name": "EnumAITextToolService",
+ "ofType": null
+ }
+ },
+ "defaultValue": null
+ }
+ ],
+ "type": {
+ "kind": "OBJECT",
+ "name": "AIAssistanceTextToolsPayload",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
{
"name": "channelEmailAdd",
"description": "Create a new email channel. This does not perform email validation.",
diff --git a/app/policies/controllers/ai_assistance_controller_policy.rb b/app/policies/controllers/ai_assistance_controller_policy.rb
new file mode 100644
index 0000000000..7b4d748a54
--- /dev/null
+++ b/app/policies/controllers/ai_assistance_controller_policy.rb
@@ -0,0 +1,5 @@
+# Copyright (C) 2012-2025 Zammad Foundation, https://zammad-foundation.org/
+
+class Controllers::AIAssistanceControllerPolicy < Controllers::ApplicationControllerPolicy
+ default_permit!('ticket.agent')
+end
diff --git a/app/services/service/ai_assistance/text_tools.rb b/app/services/service/ai_assistance/text_tools.rb
new file mode 100644
index 0000000000..1b27adbcf1
--- /dev/null
+++ b/app/services/service/ai_assistance/text_tools.rb
@@ -0,0 +1,36 @@
+# Copyright (C) 2012-2025 Zammad Foundation, https://zammad-foundation.org/
+
+class Service::AIAssistance::TextTools < Service::BaseWithCurrentUser
+ attr_reader :input, :service_type
+
+ def initialize(input:, service_type:, current_user: nil)
+ super(current_user:) if current_user.present?
+
+ @input = input
+ @service_type = service_type
+ end
+
+ def execute
+ return if input.blank?
+
+ Service::CheckFeatureEnabled.new(name: 'ai_assistance_text_tools').execute
+ Service::CheckFeatureEnabled.new(name: 'ai_provider', custom_error_message: __('AI provider is not configured.')).execute
+
+ text_tool = ai_text_tool_service_class.new(
+ current_user:,
+ context_data: {
+ input:
+ }
+ )
+
+ text_tool.execute
+ end
+
+ private
+
+ def ai_text_tool_service_class
+ "AI::Service::Text#{service_type.classify}".constantize
+ rescue
+ raise ArgumentError, __("AI assistance text tool service type '#{service_type}' is not supported.")
+ end
+end
diff --git a/config/routes/ai_assistance.rb b/config/routes/ai_assistance.rb
new file mode 100644
index 0000000000..d91bfbbe1c
--- /dev/null
+++ b/config/routes/ai_assistance.rb
@@ -0,0 +1,13 @@
+# Copyright (C) 2012-2025 Zammad Foundation, https://zammad-foundation.org/
+
+Zammad::Application.routes.draw do
+ api_path = Rails.configuration.api_path
+
+ scope api_path do
+ resources :ai_assistance, only: [] do
+ collection do
+ post :text_tools
+ end
+ end
+ end
+end
diff --git a/db/migrate/20250507155325_add_ai_assistance_text_tools.rb b/db/migrate/20250507155325_add_ai_assistance_text_tools.rb
new file mode 100644
index 0000000000..0dcd190c2c
--- /dev/null
+++ b/db/migrate/20250507155325_add_ai_assistance_text_tools.rb
@@ -0,0 +1,47 @@
+# Copyright (C) 2012-2025 Zammad Foundation, https://zammad-foundation.org/
+
+class AddAIAssistanceTextTools < ActiveRecord::Migration[7.2]
+ def change
+ # return if it's a new setup
+ return if !Setting.exists?(name: 'system_init_done')
+
+ add_admin_permission
+ add_feature_flag_setting
+ migrate_article_body_attribute
+ end
+
+ private
+
+ def add_admin_permission
+ Permission.create_if_not_exists(
+ name: 'admin.ai_assistance_text_tools',
+ label: 'Ticket Tools',
+ description: 'Manage Zammad Smart Assist text tools of your system.',
+ preferences: { prio: 1335 }
+ )
+ end
+
+ def add_feature_flag_setting
+ Setting.create_if_not_exists(
+ title: 'Text Tools',
+ name: 'ai_assistance_text_tools',
+ area: 'AI::Assistance',
+ description: 'Enable or disable the AI assistance text tools.',
+ options: {},
+ state: true,
+ preferences: {
+ authentication: true,
+ permission: ['admin.ai_assistance_text_tools'],
+ },
+ frontend: true,
+ )
+ end
+
+ def migrate_article_body_attribute
+ attribute = ObjectManager::Attribute.find_by(object_lookup_id: ObjectLookup.by_name('TicketArticle'), name: 'body')
+ return if attribute.blank?
+
+ attribute.data_option['text_tools'] = true
+ attribute.save!
+ end
+end
diff --git a/db/seeds/object_manager_attributes.rb b/db/seeds/object_manager_attributes.rb
index 6d950452d6..93509bb88c 100644
--- a/db/seeds/object_manager_attributes.rb
+++ b/db/seeds/object_manager_attributes.rb
@@ -536,11 +536,12 @@ ObjectManager::Attribute.add(
display: __('Text'),
data_type: 'richtext',
data_option: {
- type: 'richtext',
- maxlength: 150_000,
- upload: true,
- rows: 8,
- null: true,
+ type: 'richtext',
+ maxlength: 150_000,
+ upload: true,
+ rows: 8,
+ null: true,
+ text_tools: true,
},
editable: false,
active: true,
diff --git a/db/seeds/permissions.rb b/db/seeds/permissions.rb
index 23330bb52c..8173815183 100644
--- a/db/seeds/permissions.rb
+++ b/db/seeds/permissions.rb
@@ -234,6 +234,12 @@ Permission.create_if_not_exists(
description: __('Manage Zammad Smart Assist ticket summarization of your system.'),
preferences: { prio: 1334 }
)
+Permission.create_if_not_exists(
+ name: 'admin.ai_assistance_text_tools',
+ label: __('Ticket Tools'),
+ description: __('Manage Zammad Smart Assist text tools of your system.'),
+ preferences: { prio: 1335 }
+)
Permission.create_if_not_exists(
name: 'admin.integration',
label: __('Integrations'),
diff --git a/db/seeds/settings.rb b/db/seeds/settings.rb
index f46490e513..567b0cea82 100644
--- a/db/seeds/settings.rb
+++ b/db/seeds/settings.rb
@@ -6100,3 +6100,17 @@ Setting.create_if_not_exists(
},
frontend: true,
)
+
+Setting.create_if_not_exists(
+ title: __('Text Tools'),
+ name: 'ai_assistance_text_tools',
+ area: 'AI::Assistance',
+ description: __('Enable or disable the AI assistance text tools.'),
+ options: {},
+ state: true,
+ preferences: {
+ authentication: true,
+ permission: ['admin.ai_assistance_text_tools'],
+ },
+ frontend: true,
+)
diff --git a/i18n/zammad.pot b/i18n/zammad.pot
index 0f5e0d9172..c8d1a86c92 100644
--- a/i18n/zammad.pot
+++ b/i18n/zammad.pot
@@ -253,6 +253,10 @@ msgstr ""
msgid "%s hours ago"
msgstr ""
+#: app/frontend/shared/components/Form/fields/FieldEditor/AiAssistantTextTools/AiAssistantLoadingBanner.vue:33
+msgid "%s is generating text"
+msgstr ""
+
#: app/assets/javascripts/app/views/channel/email_account_overview.jst.eco:91
#: app/assets/javascripts/app/views/channel/sms_account_overview.jst.eco:58
#: app/assets/javascripts/app/views/google/list.jst.eco:61
@@ -588,7 +592,7 @@ msgstr ""
msgid "A color scheme that uses light-colored elements on a dark background."
msgstr ""
-#: db/seeds/object_manager_attributes.rb:2231
+#: db/seeds/object_manager_attributes.rb:2232
msgid "A group's email address determines which address should be used for outgoing mails, e.g. when an agent is composing an email or a trigger is sending an auto-reply"
msgstr ""
@@ -671,6 +675,7 @@ msgstr ""
#: app/controllers/ticket/summarize_controller.rb:8
#: app/graphql/gql/mutations/ticket/ai_assistance/summarize.rb:20
+#: app/services/service/ai_assistance/text_tools.rb:17
#: app/services/service/ticket/ai_assistance/summarize.rb:15
msgid "AI provider is not configured."
msgstr ""
@@ -685,7 +690,7 @@ msgstr ""
#: app/assets/javascripts/app/controllers/api.coffee:3
#: app/assets/javascripts/app/views/api.jst.eco:2
-#: db/seeds/permissions.rb:245
+#: db/seeds/permissions.rb:251
msgid "API"
msgstr ""
@@ -717,7 +722,7 @@ msgstr ""
#: lib/ai/provider/azure.rb:104
#: lib/ai/provider/ollama.rb:86
#: lib/ai/provider/open_ai.rb:93
-#: lib/ai/provider/zammad_ai.rb:52
+#: lib/ai/provider/zammad_ai.rb:53
msgid "API server not accessible"
msgstr ""
@@ -745,47 +750,47 @@ msgstr ""
msgid "Access denied"
msgstr ""
-#: db/seeds/permissions.rb:333
+#: db/seeds/permissions.rb:339
msgid "Access the agent chat features."
msgstr ""
-#: db/seeds/permissions.rb:348
+#: db/seeds/permissions.rb:354
msgid "Access the agent phone features."
msgstr ""
-#: db/seeds/permissions.rb:363
+#: db/seeds/permissions.rb:369
msgid "Access the knowledge base editor features."
msgstr ""
-#: db/seeds/permissions.rb:369
+#: db/seeds/permissions.rb:375
msgid "Access the knowledge base reader features."
msgstr ""
-#: db/seeds/permissions.rb:391
+#: db/seeds/permissions.rb:397
msgid "Access the tickets as agent based on group access."
msgstr ""
-#: db/seeds/permissions.rb:400
+#: db/seeds/permissions.rb:406
msgid "Access tickets as customer."
msgstr ""
-#: db/seeds/permissions.rb:324
+#: db/seeds/permissions.rb:330
msgid "Access to the chat interface."
msgstr ""
-#: db/seeds/permissions.rb:354
+#: db/seeds/permissions.rb:360
msgid "Access to the knowledge base interface."
msgstr ""
-#: db/seeds/permissions.rb:339
+#: db/seeds/permissions.rb:345
msgid "Access to the phone interface."
msgstr ""
-#: db/seeds/permissions.rb:376
+#: db/seeds/permissions.rb:382
msgid "Access to the report interface."
msgstr ""
-#: db/seeds/permissions.rb:382
+#: db/seeds/permissions.rb:388
msgid "Access to the ticket interface."
msgstr ""
@@ -949,7 +954,7 @@ msgstr ""
#: app/assets/javascripts/app/views/profile/password.jst.eco:29
#: app/frontend/apps/desktop/pages/personal-setting/views/PersonalSettingOutOfOffice.vue:71
#: app/frontend/apps/desktop/pages/personal-setting/views/PersonalSettingTwoFactorAuth.vue:229
-#: db/seeds/object_manager_attributes.rb:1665
+#: db/seeds/object_manager_attributes.rb:1666
msgid "Active"
msgstr ""
@@ -1133,7 +1138,7 @@ msgstr ""
msgid "Add attachment option to upload."
msgstr ""
-#: app/frontend/shared/components/Form/fields/FieldEditor/useEditorActions.ts:270
+#: app/frontend/shared/components/Form/fields/FieldEditor/useEditorActions.ts:297
msgid "Add bullet list"
msgstr ""
@@ -1154,7 +1159,7 @@ msgstr ""
msgid "Add empty checklist"
msgstr ""
-#: app/frontend/shared/components/Form/fields/FieldEditor/useEditorActions.ts:192
+#: app/frontend/shared/components/Form/fields/FieldEditor/useEditorActions.ts:219
msgid "Add first level heading"
msgstr ""
@@ -1162,11 +1167,11 @@ msgstr ""
msgid "Add from a template"
msgstr ""
-#: app/frontend/shared/components/Form/fields/FieldEditor/useEditorActions.ts:185
+#: app/frontend/shared/components/Form/fields/FieldEditor/useEditorActions.ts:212
msgid "Add heading"
msgstr ""
-#: app/frontend/shared/components/Form/fields/FieldEditor/useEditorActions.ts:130
+#: app/frontend/shared/components/Form/fields/FieldEditor/useEditorActions.ts:157
msgid "Add image"
msgstr ""
@@ -1175,7 +1180,7 @@ msgid "Add internal note"
msgstr ""
#: app/frontend/apps/desktop/pages/ticket/components/TicketSidebar/TicketSidebarInformation/TicketSidebarInformationContent/TicketLinks.vue:163
-#: app/frontend/shared/components/Form/fields/FieldEditor/useEditorActions.ts:152
+#: app/frontend/shared/components/Form/fields/FieldEditor/useEditorActions.ts:179
msgid "Add link"
msgstr ""
@@ -1189,7 +1194,7 @@ msgstr ""
msgid "Add note"
msgstr ""
-#: app/frontend/shared/components/Form/fields/FieldEditor/useEditorActions.ts:257
+#: app/frontend/shared/components/Form/fields/FieldEditor/useEditorActions.ts:284
msgid "Add ordered list"
msgstr ""
@@ -1203,7 +1208,7 @@ msgstr ""
msgid "Add reply"
msgstr ""
-#: app/frontend/shared/components/Form/fields/FieldEditor/useEditorActions.ts:203
+#: app/frontend/shared/components/Form/fields/FieldEditor/useEditorActions.ts:230
msgid "Add second level heading"
msgstr ""
@@ -1228,7 +1233,7 @@ msgstr ""
msgid "Add tag"
msgstr ""
-#: app/frontend/shared/components/Form/fields/FieldEditor/useEditorActions.ts:214
+#: app/frontend/shared/components/Form/fields/FieldEditor/useEditorActions.ts:241
msgid "Add third level heading"
msgstr ""
@@ -1260,7 +1265,7 @@ msgstr ""
msgid "Additional ticket edit actions"
msgstr ""
-#: db/seeds/object_manager_attributes.rb:1405
+#: db/seeds/object_manager_attributes.rb:1406
msgid "Address"
msgstr ""
@@ -1350,7 +1355,7 @@ msgstr ""
msgid "Agent Name + FromSeparator + System Address Display Name"
msgstr ""
-#: db/seeds/permissions.rb:332
+#: db/seeds/permissions.rb:338
msgid "Agent chat"
msgstr ""
@@ -1363,11 +1368,11 @@ msgstr ""
msgid "Agent limit exceeded, please check your account settings."
msgstr ""
-#: db/seeds/permissions.rb:347
+#: db/seeds/permissions.rb:353
msgid "Agent phone"
msgstr ""
-#: db/seeds/permissions.rb:390
+#: db/seeds/permissions.rb:396
msgid "Agent tickets"
msgstr ""
@@ -1375,6 +1380,10 @@ msgstr ""
msgid "Agents"
msgstr ""
+#: app/frontend/shared/components/Form/fields/FieldEditor/useEditorActions.ts:112
+msgid "Ai assistant text tools"
+msgstr ""
+
#: app/assets/javascripts/app/views/profile/calendar_subscriptions.jst.eco:27
msgid "Alarm"
msgstr ""
@@ -1437,7 +1446,7 @@ msgstr ""
msgid "Allow past dates toggle value is required."
msgstr ""
-#: db/seeds/object_manager_attributes.rb:2164
+#: db/seeds/object_manager_attributes.rb:2165
msgid "Allow reopening of tickets within a certain time."
msgstr ""
@@ -1620,7 +1629,7 @@ msgstr ""
#: app/frontend/apps/desktop/components/layout/LayoutSidebar/LeftSidebar/AvatarMenu/plugins/appearance.ts:9
#: app/frontend/apps/desktop/pages/personal-setting/views/PersonalSetting/plugins/appearance.ts:6
#: app/frontend/apps/desktop/pages/personal-setting/views/PersonalSettingAppearance.vue:60
-#: db/seeds/permissions.rb:413
+#: db/seeds/permissions.rb:419
msgid "Appearance"
msgstr ""
@@ -1860,12 +1869,12 @@ msgstr ""
msgid "Article#"
msgstr ""
-#: db/seeds/object_manager_attributes.rb:2186
+#: db/seeds/object_manager_attributes.rb:2187
msgid "Assign Follow-Ups"
msgstr ""
#: app/assets/javascripts/app/models/group.coffee:11
-#: db/seeds/object_manager_attributes.rb:2195
+#: db/seeds/object_manager_attributes.rb:2196
msgid "Assign follow-up to latest agent again."
msgstr ""
@@ -1877,7 +1886,7 @@ msgstr ""
msgid "Assign signup roles"
msgstr ""
-#: db/seeds/object_manager_attributes.rb:1795
+#: db/seeds/object_manager_attributes.rb:1796
msgid "Assign users based on user domain."
msgstr ""
@@ -1894,7 +1903,7 @@ msgstr ""
msgid "Assignees"
msgstr ""
-#: db/seeds/object_manager_attributes.rb:2087
+#: db/seeds/object_manager_attributes.rb:2088
msgid "Assignment Timeout"
msgstr ""
@@ -1904,7 +1913,7 @@ msgid "Assignment timeout"
msgstr ""
#: app/assets/javascripts/app/models/group.coffee:8
-#: db/seeds/object_manager_attributes.rb:2092
+#: db/seeds/object_manager_attributes.rb:2093
msgid "Assignment timeout in minutes if assigned agent is not working on it. Ticket will be shown as unassigend."
msgstr ""
@@ -2200,7 +2209,7 @@ msgstr ""
#: app/frontend/apps/mobile/pages/account/views/AccountOverview.vue:128
#: app/frontend/apps/mobile/pages/account/views/PersonalSettingAvatar.vue:175
#: app/frontend/shared/components/CommonUserAvatar/CommonUserAvatar.vue:119
-#: db/seeds/permissions.rb:427
+#: db/seeds/permissions.rb:433
msgid "Avatar"
msgstr ""
@@ -2585,6 +2594,7 @@ msgstr ""
#: app/assets/javascripts/app/controllers/ticket_zoom/article_action/email_reply.coffee:380
#: app/assets/javascripts/app/controllers/ticket_zoom/article_action/twitter_reply.coffee:177
#: app/assets/javascripts/app/controllers/ticket_zoom/article_new.coffee:339
+#: app/assets/javascripts/app/views/generic/text_tools_loading.jst.eco:3
#: app/assets/javascripts/app/views/integration/exchange_certificate_issue.jst.eco:18
#: app/assets/javascripts/app/views/integration/exchange_wizard.jst.eco:236
#: app/assets/javascripts/app/views/integration/ldap_wizard.jst.eco:292
@@ -2600,6 +2610,7 @@ msgstr ""
#: app/frontend/apps/mobile/pages/search/views/SearchOverview.vue:225
#: app/frontend/apps/mobile/pages/ticket/components/TicketDetailView/ArticleReplyDialog.vue:120
#: app/frontend/apps/mobile/pages/ticket/components/TicketDetailView/TicketAction/TicketActionChangeCustomerDialog.vue:78
+#: app/frontend/shared/components/Form/fields/FieldEditor/AiAssistantTextTools/AiAssistantLoadingBanner.vue:51
msgid "Cancel"
msgstr ""
@@ -2667,7 +2678,7 @@ msgstr ""
msgid "Cannot set online notifications as seen"
msgstr ""
-#: app/assets/javascripts/app/controllers/ticket_zoom/article_new.coffee:797
+#: app/assets/javascripts/app/controllers/ticket_zoom/article_new.coffee:803
msgid "Cannot upload file"
msgstr ""
@@ -2743,11 +2754,11 @@ msgstr ""
msgid "Change order"
msgstr ""
-#: db/seeds/permissions.rb:445
+#: db/seeds/permissions.rb:451
msgid "Change personal account password."
msgstr ""
-#: app/frontend/shared/components/Form/fields/FieldEditor/useEditorActions.ts:227
+#: app/frontend/shared/components/Form/fields/FieldEditor/useEditorActions.ts:254
msgid "Change text color"
msgstr ""
@@ -2912,7 +2923,7 @@ msgstr ""
#: app/assets/javascripts/app/controllers/checklist_template.coffee:3
#: app/assets/javascripts/app/views/checklist_template/index.jst.eco:7
-#: db/seeds/permissions.rb:317
+#: db/seeds/permissions.rb:323
#: db/seeds/settings.rb:5987
msgid "Checklists"
msgstr ""
@@ -2992,7 +3003,7 @@ msgstr ""
msgid "Christmas holiday"
msgstr ""
-#: db/seeds/object_manager_attributes.rb:1325
+#: db/seeds/object_manager_attributes.rb:1326
msgid "City"
msgstr ""
@@ -3589,7 +3600,7 @@ msgid "Core Workflow Ajax Mode"
msgstr ""
#: app/assets/javascripts/app/controllers/core_workflow.coffee:3
-#: db/seeds/permissions.rb:269
+#: db/seeds/permissions.rb:275
msgid "Core Workflows"
msgstr ""
@@ -3680,7 +3691,7 @@ msgstr ""
msgid "Counting entries. This may take a while."
msgstr ""
-#: db/seeds/object_manager_attributes.rb:1365
+#: db/seeds/object_manager_attributes.rb:1366
msgid "Country"
msgstr ""
@@ -4052,7 +4063,7 @@ msgstr ""
msgid "Customer selection based on sender and receiver list"
msgstr ""
-#: db/seeds/permissions.rb:399
+#: db/seeds/permissions.rb:405
msgid "Customer tickets"
msgstr ""
@@ -4061,7 +4072,7 @@ msgid "Customer tickets of the user will get deleted on execution of the task. N
msgstr ""
#: app/assets/javascripts/app/models/organization.coffee:7
-#: db/seeds/object_manager_attributes.rb:1749
+#: db/seeds/object_manager_attributes.rb:1750
msgid "Customers in the organization can view each other's items."
msgstr ""
@@ -4094,7 +4105,7 @@ msgstr ""
#: app/assets/javascripts/app/controllers/data_privacy.coffee:3
#: app/assets/javascripts/app/views/data_privacy/index.jst.eco:3
-#: db/seeds/permissions.rb:281
+#: db/seeds/permissions.rb:287
msgid "Data Privacy"
msgstr ""
@@ -5030,7 +5041,7 @@ msgstr ""
msgid "Delete column"
msgstr ""
-#: db/seeds/permissions.rb:282
+#: db/seeds/permissions.rb:288
msgid "Delete existing data of your system."
msgstr ""
@@ -5135,7 +5146,7 @@ msgstr ""
msgid "Delivery failed: \"%s\""
msgstr ""
-#: db/seeds/object_manager_attributes.rb:1206
+#: db/seeds/object_manager_attributes.rb:1207
msgid "Department"
msgstr ""
@@ -5217,7 +5228,7 @@ msgid "Detect Duplicate Ticket Creation"
msgstr ""
#: app/assets/javascripts/app/views/ticket_zoom/article_view.jst.eco:158
-#: db/seeds/object_manager_attributes.rb:734
+#: db/seeds/object_manager_attributes.rb:735
msgid "Detected Language"
msgstr ""
@@ -5242,7 +5253,7 @@ msgstr ""
#: app/assets/javascripts/app/views/profile/devices.jst.eco:2
#: app/frontend/apps/desktop/pages/personal-setting/views/PersonalSetting/plugins/devices.ts:6
#: app/frontend/apps/desktop/pages/personal-setting/views/PersonalSettingDevices.vue:34
-#: db/seeds/permissions.rb:458
+#: db/seeds/permissions.rb:464
msgid "Devices"
msgstr ""
@@ -5457,7 +5468,7 @@ msgstr ""
msgid "Document file"
msgstr ""
-#: db/seeds/object_manager_attributes.rb:1790
+#: db/seeds/object_manager_attributes.rb:1791
msgid "Domain"
msgstr ""
@@ -5466,7 +5477,7 @@ msgstr ""
msgid "Domain Alias"
msgstr ""
-#: db/seeds/object_manager_attributes.rb:1790
+#: db/seeds/object_manager_attributes.rb:1791
msgid "Domain based assignment"
msgstr ""
@@ -5773,7 +5784,7 @@ msgstr ""
#: app/frontend/apps/desktop/pages/guided-setup/components/GuidedSetupImport/GuidedSetupImportSource/GuidedSetupImportSourceZendesk.vue:34
#: app/frontend/apps/desktop/pages/ticket/components/TicketDetailView/article-type/plugins/email.ts:11
#: app/frontend/shared/entities/ticket-article/action/plugins/email.ts:167
-#: db/seeds/object_manager_attributes.rb:902
+#: db/seeds/object_manager_attributes.rb:903
#: db/seeds/permissions.rb:131
#: public/assets/form/form.js:32
msgid "Email"
@@ -5978,6 +5989,10 @@ msgstr ""
msgid "Enable or disable self-shutdown of Zammad processes after significant configuration changes. This should only be used if the controlling process manager like systemd or docker supports an automatic restart policy."
msgstr ""
+#: db/seeds/settings.rb:6101
+msgid "Enable or disable the AI assistance text tools."
+msgstr ""
+
#: db/seeds/settings.rb:6070
msgid "Enable or disable the AI assistance ticket summary."
msgstr ""
@@ -6196,7 +6211,7 @@ msgstr ""
msgid "Enter credentials"
msgstr ""
-#: app/frontend/shared/components/Form/fields/FieldEditor/useEditorActions.ts:157
+#: app/frontend/shared/components/Form/fields/FieldEditor/useEditorActions.ts:184
msgid "Enter link URL"
msgstr ""
@@ -6473,6 +6488,11 @@ msgstr ""
msgid "Existing tickets (open)"
msgstr ""
+#: app/assets/javascripts/app/views/generic/text_tools.jst.eco:14
+#: app/frontend/shared/components/Form/fields/FieldEditor/AiAssistantTextTools/AiAssistantTextTools.vue:171
+msgid "Expand"
+msgstr ""
+
#: app/frontend/apps/desktop/components/layout/LayoutSidebar.vue:198
msgid "Expand sidebar"
msgstr ""
@@ -6622,7 +6642,7 @@ msgid "Failed to upload."
msgstr ""
#: app/frontend/apps/desktop/pages/ticket/components/TicketDetailView/article-type/plugins/fax.ts:7
-#: db/seeds/object_manager_attributes.rb:1074
+#: db/seeds/object_manager_attributes.rb:1075
msgid "Fax"
msgstr ""
@@ -6673,7 +6693,7 @@ msgstr ""
msgid "File format is not allowed: %s"
msgstr ""
-#: app/assets/javascripts/app/controllers/ticket_zoom/article_new.coffee:858
+#: app/assets/javascripts/app/controllers/ticket_zoom/article_new.coffee:864
#: app/frontend/shared/components/Form/fields/FieldFile/composable/useFileValidation.ts:26
#: lib/validations/ticket_article_validator/whatsapp_message.rb:38
msgid "File is too big. %s has to be %s or smaller."
@@ -6765,7 +6785,7 @@ msgstr ""
#: app/assets/javascripts/app/models/user.coffee:9
#: app/frontend/apps/desktop/composables/authentication/useSignupForm.ts:14
-#: db/seeds/object_manager_attributes.rb:798
+#: db/seeds/object_manager_attributes.rb:799
msgid "First name"
msgstr ""
@@ -6775,6 +6795,11 @@ msgstr ""
msgid "First response"
msgstr ""
+#: app/assets/javascripts/app/views/generic/text_tools.jst.eco:12
+#: app/frontend/shared/components/Form/fields/FieldEditor/AiAssistantTextTools/AiAssistantTextTools.vue:165
+msgid "Fix spelling and grammar"
+msgstr ""
+
#: app/assets/javascripts/app/views/channel/email_account_overview.jst.eco:3
msgid "Fixed Email Accounts"
msgstr ""
@@ -6803,12 +6828,12 @@ msgid "Folders"
msgstr ""
#: app/assets/javascripts/app/models/group.coffee:9
-#: db/seeds/object_manager_attributes.rb:2130
+#: db/seeds/object_manager_attributes.rb:2131
msgid "Follow-up for closed ticket possible or not."
msgstr ""
#: app/assets/javascripts/app/models/group.coffee:9
-#: db/seeds/object_manager_attributes.rb:2120
+#: db/seeds/object_manager_attributes.rb:2121
msgid "Follow-up possible"
msgstr ""
@@ -6898,7 +6923,7 @@ msgstr ""
msgid "Format as _underlined_"
msgstr ""
-#: app/frontend/shared/components/Form/fields/FieldEditor/useEditorActions.ts:98
+#: app/frontend/shared/components/Form/fields/FieldEditor/useEditorActions.ts:125
msgid "Format as bold"
msgstr ""
@@ -6914,7 +6939,7 @@ msgstr ""
msgid "Format as h3 heading"
msgstr ""
-#: app/frontend/shared/components/Form/fields/FieldEditor/useEditorActions.ts:106
+#: app/frontend/shared/components/Form/fields/FieldEditor/useEditorActions.ts:133
msgid "Format as italic"
msgstr ""
@@ -6922,11 +6947,11 @@ msgstr ""
msgid "Format as ordered list"
msgstr ""
-#: app/frontend/shared/components/Form/fields/FieldEditor/useEditorActions.ts:122
+#: app/frontend/shared/components/Form/fields/FieldEditor/useEditorActions.ts:149
msgid "Format as strikethrough"
msgstr ""
-#: app/frontend/shared/components/Form/fields/FieldEditor/useEditorActions.ts:114
+#: app/frontend/shared/components/Form/fields/FieldEditor/useEditorActions.ts:141
msgid "Format as underlined"
msgstr ""
@@ -7058,6 +7083,10 @@ msgstr ""
msgid "Generating recovery codes…"
msgstr ""
+#: app/frontend/shared/components/Form/fields/FieldEditor/AiAssistantTextTools/AiAssistantLoadingBanner.vue:44
+msgid "Generating text…"
+msgstr ""
+
#: app/assets/javascripts/app/controllers/_integration/cti.coffee:264
msgid "Generic API to integrate VoIP service provider with real-time push."
msgstr ""
@@ -7333,7 +7362,7 @@ msgstr ""
#: app/assets/javascripts/app/models/role.coffee:8
#: app/assets/javascripts/app/models/user.coffee:13
-#: db/seeds/object_manager_attributes.rb:1622
+#: db/seeds/object_manager_attributes.rb:1623
msgid "Group permissions"
msgstr ""
@@ -7859,7 +7888,7 @@ msgstr ""
#: app/assets/javascripts/app/controllers/_ui_element/richtext_additions/popup_image.coffee:18
#: app/assets/javascripts/app/lib/app_post/image_service.coffee:134
-#: app/assets/javascripts/app/lib/base/jquery.contenteditable.js:360
+#: app/assets/javascripts/app/lib/base/jquery.contenteditable.js:421
msgid "Image file size is too large, please try inserting a smaller file."
msgstr ""
@@ -7966,6 +7995,11 @@ msgstr ""
msgid "Imported"
msgstr ""
+#: app/assets/javascripts/app/views/generic/text_tools.jst.eco:10
+#: app/frontend/shared/components/Form/fields/FieldEditor/AiAssistantTextTools/AiAssistantTextTools.vue:159
+msgid "Improve writing"
+msgstr ""
+
#: app/assets/javascripts/app/views/integration/placetel.jst.eco:22
msgid "In order for Zammad to access %s, the %s API token must be stored here:"
msgstr ""
@@ -8085,7 +8119,7 @@ msgstr ""
msgid "Increment seconds value"
msgstr ""
-#: app/frontend/shared/components/Form/fields/FieldEditor/useEditorActions.ts:235
+#: app/frontend/shared/components/Form/fields/FieldEditor/useEditorActions.ts:262
msgid "Indent text"
msgstr ""
@@ -8128,7 +8162,7 @@ msgstr ""
msgid "Input field must be text, password, tel, fax, email or url type."
msgstr ""
-#: app/frontend/shared/components/Form/fields/FieldEditor/useEditorActions.ts:319
+#: app/frontend/shared/components/Form/fields/FieldEditor/useEditorActions.ts:346
msgid "Insert code block"
msgstr ""
@@ -8140,7 +8174,7 @@ msgstr ""
msgid "Insert column before"
msgstr ""
-#: app/frontend/shared/components/Form/fields/FieldEditor/useEditorActions.ts:308
+#: app/frontend/shared/components/Form/fields/FieldEditor/useEditorActions.ts:335
msgid "Insert inline code"
msgstr ""
@@ -8152,15 +8186,15 @@ msgstr ""
msgid "Insert row below"
msgstr ""
-#: app/frontend/shared/components/Form/fields/FieldEditor/useEditorActions.ts:291
+#: app/frontend/shared/components/Form/fields/FieldEditor/useEditorActions.ts:318
msgid "Insert table"
msgstr ""
-#: app/frontend/shared/components/Form/fields/FieldEditor/useEditorActions.ts:342
+#: app/frontend/shared/components/Form/fields/FieldEditor/useEditorActions.ts:369
msgid "Insert text from Knowledge Base article"
msgstr ""
-#: app/frontend/shared/components/Form/fields/FieldEditor/useEditorActions.ts:350
+#: app/frontend/shared/components/Form/fields/FieldEditor/useEditorActions.ts:377
msgid "Insert text from text module"
msgstr ""
@@ -8201,7 +8235,7 @@ msgid "Integer field"
msgstr ""
#: app/assets/javascripts/app/controllers/integrations.coffee:3
-#: db/seeds/permissions.rb:239
+#: db/seeds/permissions.rb:245
msgid "Integrations"
msgstr ""
@@ -8560,7 +8594,7 @@ msgstr ""
msgid "Knowledge Base Answer"
msgstr ""
-#: db/seeds/permissions.rb:362
+#: db/seeds/permissions.rb:368
msgid "Knowledge Base Editor"
msgstr ""
@@ -8568,7 +8602,7 @@ msgstr ""
msgid "Knowledge Base Feed"
msgstr ""
-#: db/seeds/permissions.rb:368
+#: db/seeds/permissions.rb:374
msgid "Knowledge Base Reader"
msgstr ""
@@ -8622,7 +8656,7 @@ msgstr ""
#: app/frontend/apps/desktop/pages/personal-setting/views/PersonalSetting/plugins/locale.ts:6
#: app/frontend/apps/desktop/pages/personal-setting/views/PersonalSettingLocale.vue:13
#: app/frontend/apps/mobile/pages/account/views/AccountOverview.vue:157
-#: db/seeds/permissions.rb:420
+#: db/seeds/permissions.rb:426
msgid "Language"
msgstr ""
@@ -8713,7 +8747,7 @@ msgstr ""
#: app/assets/javascripts/app/models/user.coffee:10
#: app/frontend/apps/desktop/composables/authentication/useSignupForm.ts:23
-#: db/seeds/object_manager_attributes.rb:850
+#: db/seeds/object_manager_attributes.rb:851
msgid "Last name"
msgstr ""
@@ -8880,7 +8914,7 @@ msgstr ""
#: app/assets/javascripts/app/views/widget/user.jst.eco:34
#: app/frontend/apps/desktop/pages/personal-setting/views/PersonalSetting/plugins/linkedAccounts.ts:8
#: app/frontend/apps/desktop/pages/personal-setting/views/PersonalSettingLinkedAccounts.vue:56
-#: db/seeds/permissions.rb:472
+#: db/seeds/permissions.rb:478
msgid "Linked Accounts"
msgstr ""
@@ -9013,7 +9047,7 @@ msgstr ""
#: app/assets/javascripts/app/views/maintenance.jst.eco:25
#: app/controllers/time_accountings_controller.rb:101
#: app/frontend/apps/desktop/components/User/UserListTable.vue:57
-#: db/seeds/object_manager_attributes.rb:766
+#: db/seeds/object_manager_attributes.rb:767
msgid "Login"
msgstr ""
@@ -9085,7 +9119,7 @@ msgstr ""
#: app/assets/javascripts/app/controllers/maintenance.coffee:3
#: app/assets/javascripts/app/views/maintenance.jst.eco:3
-#: db/seeds/permissions.rb:287
+#: db/seeds/permissions.rb:293
msgid "Maintenance"
msgstr ""
@@ -9110,7 +9144,7 @@ msgstr ""
msgid "Manage AI settings of your system."
msgstr ""
-#: db/seeds/permissions.rb:246
+#: db/seeds/permissions.rb:252
msgid "Manage API of your system."
msgstr ""
@@ -9167,15 +9201,19 @@ msgstr ""
msgid "Manage WhatsApp channel of your system."
msgstr ""
+#: db/seeds/permissions.rb:240
+msgid "Manage Zammad Smart Assist text tools of your system."
+msgstr ""
+
#: db/seeds/permissions.rb:234
msgid "Manage Zammad Smart Assist ticket summarization of your system."
msgstr ""
-#: db/seeds/permissions.rb:511
+#: db/seeds/permissions.rb:517
msgid "Manage access to New BETA UI switch."
msgstr ""
-#: db/seeds/permissions.rb:306
+#: db/seeds/permissions.rb:312
msgid "Manage active user sessions of your system."
msgstr ""
@@ -9207,7 +9245,7 @@ msgstr ""
msgid "Manage core system settings."
msgstr ""
-#: db/seeds/permissions.rb:270
+#: db/seeds/permissions.rb:276
msgid "Manage core workflows of your system."
msgstr ""
@@ -9223,71 +9261,71 @@ msgstr ""
msgid "Manage groups of your system."
msgstr ""
-#: db/seeds/permissions.rb:240
+#: db/seeds/permissions.rb:246
msgid "Manage integrations of your system."
msgstr ""
-#: db/seeds/permissions.rb:288
+#: db/seeds/permissions.rb:294
msgid "Manage maintenance mode of your system."
msgstr ""
-#: db/seeds/permissions.rb:294
+#: db/seeds/permissions.rb:300
msgid "Manage monitoring of your system."
msgstr ""
-#: db/seeds/permissions.rb:252
+#: db/seeds/permissions.rb:258
msgid "Manage object attributes of your system."
msgstr ""
-#: db/seeds/permissions.rb:300
+#: db/seeds/permissions.rb:306
msgid "Manage packages of your system."
msgstr ""
-#: db/seeds/permissions.rb:466
+#: db/seeds/permissions.rb:472
msgid "Manage personal API tokens."
msgstr ""
-#: db/seeds/permissions.rb:414
+#: db/seeds/permissions.rb:420
msgid "Manage personal appearance settings."
msgstr ""
-#: db/seeds/permissions.rb:428
+#: db/seeds/permissions.rb:434
msgid "Manage personal avatar settings."
msgstr ""
-#: db/seeds/permissions.rb:500
+#: db/seeds/permissions.rb:506
msgid "Manage personal calendar."
msgstr ""
-#: db/seeds/permissions.rb:459
+#: db/seeds/permissions.rb:465
msgid "Manage personal devices and sessions."
msgstr ""
-#: db/seeds/permissions.rb:421
+#: db/seeds/permissions.rb:427
msgid "Manage personal language settings."
msgstr ""
-#: db/seeds/permissions.rb:473
+#: db/seeds/permissions.rb:479
msgid "Manage personal linked accounts."
msgstr ""
-#: db/seeds/permissions.rb:480
+#: db/seeds/permissions.rb:486
msgid "Manage personal notifications settings."
msgstr ""
-#: db/seeds/permissions.rb:435
+#: db/seeds/permissions.rb:441
msgid "Manage personal out of office settings."
msgstr ""
-#: db/seeds/permissions.rb:490
+#: db/seeds/permissions.rb:496
msgid "Manage personal overviews."
msgstr ""
-#: db/seeds/permissions.rb:407
+#: db/seeds/permissions.rb:413
msgid "Manage personal settings."
msgstr ""
-#: db/seeds/permissions.rb:452
+#: db/seeds/permissions.rb:458
msgid "Manage personal two-factor authentication methods."
msgstr ""
@@ -9311,7 +9349,7 @@ msgstr ""
msgid "Manage security settings of your system."
msgstr ""
-#: db/seeds/permissions.rb:312
+#: db/seeds/permissions.rb:318
msgid "Manage system report of your system."
msgstr ""
@@ -9323,7 +9361,7 @@ msgstr ""
msgid "Manage ticket auto assignment settings of your system."
msgstr ""
-#: db/seeds/permissions.rb:318
+#: db/seeds/permissions.rb:324
msgid "Manage ticket checklists of your system."
msgstr ""
@@ -9339,7 +9377,7 @@ msgstr ""
msgid "Manage ticket overviews of your system."
msgstr ""
-#: db/seeds/permissions.rb:264
+#: db/seeds/permissions.rb:270
msgid "Manage ticket priorities of your system."
msgstr ""
@@ -9347,7 +9385,7 @@ msgstr ""
msgid "Manage ticket settings of your system."
msgstr ""
-#: db/seeds/permissions.rb:258
+#: db/seeds/permissions.rb:264
msgid "Manage ticket states of your system."
msgstr ""
@@ -9363,7 +9401,7 @@ msgstr ""
msgid "Manage time accounting settings of your system."
msgstr ""
-#: db/seeds/permissions.rb:276
+#: db/seeds/permissions.rb:282
msgid "Manage translations of your system."
msgstr ""
@@ -9557,7 +9595,7 @@ msgstr ""
msgid "Mention for"
msgstr ""
-#: app/frontend/shared/components/Form/fields/FieldEditor/useEditorActions.ts:334
+#: app/frontend/shared/components/Form/fields/FieldEditor/useEditorActions.ts:361
msgid "Mention user"
msgstr ""
@@ -9756,7 +9794,7 @@ msgid "Mismatching phone number id."
msgstr ""
#: app/assets/javascripts/app/views/channel/chat.jst.eco:23
-#: db/seeds/object_manager_attributes.rb:1034
+#: db/seeds/object_manager_attributes.rb:1035
msgid "Mobile"
msgstr ""
@@ -9790,7 +9828,7 @@ msgstr ""
#: app/assets/javascripts/app/controllers/monitoring.coffee:3
#: app/assets/javascripts/app/views/monitoring.jst.eco:3
-#: db/seeds/permissions.rb:293
+#: db/seeds/permissions.rb:299
msgid "Monitoring"
msgstr ""
@@ -9980,7 +10018,7 @@ msgstr ""
#: app/frontend/apps/desktop/pages/ticket/components/TicketSharedDraftFlyout.vue:192
#: app/frontend/apps/desktop/pages/ticket/components/TicketSidebar/TicketSidebarExternalReferences/TicketSidebarIdoit/IdoitFlyout/IdoitObjectList.vue:20
#: app/frontend/apps/desktop/pages/ticket/components/TicketSidebar/TicketSidebarSharedDraftStart/TicketSidebarSharedDraftStartContent.vue:165
-#: db/seeds/object_manager_attributes.rb:1704
+#: db/seeds/object_manager_attributes.rb:1705
#: public/assets/form/form.js:24
msgid "Name"
msgstr ""
@@ -10095,7 +10133,7 @@ msgstr ""
msgid "New BETA UI"
msgstr ""
-#: db/seeds/permissions.rb:510
+#: db/seeds/permissions.rb:516
msgid "New BETA UI Switch"
msgstr ""
@@ -10713,7 +10751,7 @@ msgstr ""
#: app/frontend/apps/desktop/components/Ticket/TicketBulkEditFlyout/TicketBulkEditFlyout.vue:106
#: app/frontend/apps/desktop/pages/ticket/components/TicketDetailView/article-type/plugins/note.ts:7
#: app/frontend/shared/entities/ticket-article/action/plugins/note.ts:14
-#: db/seeds/object_manager_attributes.rb:1532
+#: db/seeds/object_manager_attributes.rb:1533
msgid "Note"
msgstr ""
@@ -10734,7 +10772,7 @@ msgstr ""
#: app/assets/javascripts/app/models/job.coffee:15
#: app/assets/javascripts/app/models/role.coffee:10
#: app/assets/javascripts/app/models/signature.coffee:26
-#: db/seeds/object_manager_attributes.rb:1539
+#: db/seeds/object_manager_attributes.rb:1540
msgid "Notes are visible to agents only, never to customers."
msgstr ""
@@ -10807,7 +10845,7 @@ msgstr ""
#: app/frontend/apps/desktop/pages/personal-setting/views/PersonalSetting/plugins/notifications.ts:6
#: app/frontend/apps/desktop/pages/personal-setting/views/PersonalSettingNotifications.vue:34
#: app/frontend/apps/mobile/pages/online-notification/routes.ts:8
-#: db/seeds/permissions.rb:479
+#: db/seeds/permissions.rb:485
msgid "Notifications"
msgstr ""
@@ -10908,7 +10946,7 @@ msgstr ""
#: app/assets/javascripts/app/controllers/object_manager.coffee:330
#: app/frontend/apps/desktop/pages/ticket/components/TicketSidebar/TicketSidebarExternalReferences/TicketSidebarIdoit/TicketSidebarIdoit.vue:56
-#: db/seeds/permissions.rb:251
+#: db/seeds/permissions.rb:257
msgid "Objects"
msgstr ""
@@ -10968,7 +11006,7 @@ msgstr ""
msgid "Only %s attachment allowed"
msgstr ""
-#: app/assets/javascripts/app/controllers/ticket_zoom/article_new.coffee:823
+#: app/assets/javascripts/app/controllers/ticket_zoom/article_new.coffee:829
msgid "Only %s attachment allowed."
msgstr ""
@@ -11313,7 +11351,7 @@ msgstr ""
#: app/assets/javascripts/app/views/profile/out_of_office.jst.eco:2
#: app/frontend/apps/desktop/pages/personal-setting/views/PersonalSetting/plugins/outOfOffice.ts:6
#: app/frontend/apps/desktop/pages/personal-setting/views/PersonalSettingOutOfOffice.vue:126
-#: db/seeds/permissions.rb:434
+#: db/seeds/permissions.rb:440
msgid "Out of Office"
msgstr ""
@@ -11372,7 +11410,7 @@ msgstr ""
msgid "Outdated Browser"
msgstr ""
-#: app/frontend/shared/components/Form/fields/FieldEditor/useEditorActions.ts:246
+#: app/frontend/shared/components/Form/fields/FieldEditor/useEditorActions.ts:273
msgid "Outdent text"
msgstr ""
@@ -11469,7 +11507,7 @@ msgid "Package"
msgstr ""
#: app/assets/javascripts/app/controllers/package.coffee:3
-#: db/seeds/permissions.rb:299
+#: db/seeds/permissions.rb:305
msgid "Packages"
msgstr ""
@@ -11499,7 +11537,7 @@ msgstr ""
msgid "Parent"
msgstr ""
-#: db/seeds/object_manager_attributes.rb:2054
+#: db/seeds/object_manager_attributes.rb:2055
msgid "Parent group"
msgstr ""
@@ -11525,8 +11563,8 @@ msgstr ""
#: app/frontend/apps/desktop/pages/personal-setting/views/PersonalSetting/plugins/password.ts:8
#: app/frontend/apps/desktop/pages/personal-setting/views/PersonalSettingPassword.vue:32
#: app/frontend/apps/mobile/pages/authentication/components/LoginCredentialsForm.vue:52
-#: db/seeds/object_manager_attributes.rb:1446
-#: db/seeds/permissions.rb:444
+#: db/seeds/object_manager_attributes.rb:1447
+#: db/seeds/permissions.rb:450
msgid "Password"
msgstr ""
@@ -11691,8 +11729,8 @@ msgstr ""
#: app/assets/javascripts/app/controllers/cti.coffee:322
#: app/frontend/apps/desktop/pages/ticket/components/TicketDetailView/article-type/plugins/phone.ts:7
#: app/frontend/shared/entities/ticket-article/action/plugins/phone.ts:12
-#: db/seeds/object_manager_attributes.rb:994
-#: db/seeds/permissions.rb:338
+#: db/seeds/object_manager_attributes.rb:995
+#: db/seeds/permissions.rb:344
msgid "Phone"
msgstr ""
@@ -11882,6 +11920,10 @@ msgstr ""
msgid "Please select at least one ticket overview"
msgstr ""
+#: app/assets/javascripts/app/lib/mixins/text_tools.coffee:39
+msgid "Please select some text first."
+msgstr ""
+
#: app/assets/javascripts/app/views/integration/exchange.jst.eco:5
msgid "Please select the authentication method that should be used to establish the connection to your Exchange server."
msgstr ""
@@ -12117,7 +12159,7 @@ msgid "Profile language updated successfully."
msgstr ""
#: app/frontend/apps/desktop/components/layout/LayoutSidebar/LeftSidebar/AvatarMenu/plugins/personal-settings.ts:7
-#: db/seeds/permissions.rb:406
+#: db/seeds/permissions.rb:412
msgid "Profile settings"
msgstr ""
@@ -12486,7 +12528,7 @@ msgstr ""
msgid "Remove checklist"
msgstr ""
-#: app/frontend/shared/components/Form/fields/FieldEditor/useEditorActions.ts:283
+#: app/frontend/shared/components/Form/fields/FieldEditor/useEditorActions.ts:310
msgid "Remove formatting"
msgstr ""
@@ -12553,7 +12595,7 @@ msgid "Reopening rate"
msgstr ""
#: app/assets/javascripts/app/models/group.coffee:10
-#: db/seeds/object_manager_attributes.rb:2157
+#: db/seeds/object_manager_attributes.rb:2158
msgid "Reopening time in days"
msgstr ""
@@ -12834,7 +12876,7 @@ msgstr ""
#: app/assets/javascripts/app/controllers/role.coffee:3
#: app/assets/javascripts/app/views/integration/ldap_wizard.jst.eco:193
-#: db/seeds/object_manager_attributes.rb:1577
+#: db/seeds/object_manager_attributes.rb:1578
#: db/seeds/permissions.rb:23
msgid "Roles"
msgstr ""
@@ -13100,7 +13142,7 @@ msgstr ""
#: app/assets/javascripts/app/controllers/_integration/github.coffee:63
#: app/assets/javascripts/app/controllers/_integration/gitlab.coffee:80
#: app/assets/javascripts/app/controllers/_integration/idoit.coffee:93
-#: app/assets/javascripts/app/controllers/ticket_zoom.coffee:1135
+#: app/assets/javascripts/app/controllers/ticket_zoom.coffee:1137
msgid "Saving failed."
msgstr ""
@@ -13354,7 +13396,7 @@ msgstr ""
#: app/frontend/apps/desktop/pages/ticket/components/TicketSidebar/TicketSidebarCustomer/TicketSidebarCustomerContent.vue:101
#: app/frontend/apps/mobile/pages/ticket/views/TicketInformation/TicketInformationCustomer.vue:98
#: app/frontend/apps/mobile/pages/user/views/UserDetailView.vue:136
-#: db/seeds/object_manager_attributes.rb:1159
+#: db/seeds/object_manager_attributes.rb:1160
msgid "Secondary organizations"
msgstr ""
@@ -13641,7 +13683,7 @@ msgstr ""
msgid "Sender of last article"
msgstr ""
-#: db/seeds/object_manager_attributes.rb:2222
+#: db/seeds/object_manager_attributes.rb:2223
msgid "Sending Email Address"
msgstr ""
@@ -13712,7 +13754,7 @@ msgstr ""
#: app/assets/javascripts/app/controllers/session.coffee:3
#: app/assets/javascripts/app/views/session.jst.eco:2
-#: db/seeds/permissions.rb:305
+#: db/seeds/permissions.rb:311
msgid "Sessions"
msgstr ""
@@ -13878,7 +13920,7 @@ msgstr ""
#: app/assets/javascripts/app/controllers/agent_ticket_create/sidebar_shared_draft.coffee:12
#: app/assets/javascripts/app/models/group.coffee:17
#: app/frontend/apps/desktop/pages/ticket/components/TicketSidebar/plugins/shared-draft-start.ts:10
-#: db/seeds/object_manager_attributes.rb:2291
+#: db/seeds/object_manager_attributes.rb:2292
msgid "Shared Drafts"
msgstr ""
@@ -13915,7 +13957,7 @@ msgid "Shared mailbox"
msgstr ""
#: app/assets/javascripts/app/models/organization.coffee:7
-#: db/seeds/object_manager_attributes.rb:1744
+#: db/seeds/object_manager_attributes.rb:1745
msgid "Shared organization"
msgstr ""
@@ -14124,7 +14166,7 @@ msgstr ""
#: app/assets/javascripts/app/controllers/_channel/_email_signature.coffee:49
#: app/assets/javascripts/app/models/group.coffee:13
#: app/controllers/integration/smime_controller.rb:119
-#: db/seeds/object_manager_attributes.rb:2257
+#: db/seeds/object_manager_attributes.rb:2258
msgid "Signature"
msgstr ""
@@ -14186,6 +14228,11 @@ msgstr ""
msgid "Simple Storage Service not reachable."
msgstr ""
+#: app/assets/javascripts/app/views/generic/text_tools.jst.eco:16
+#: app/frontend/shared/components/Form/fields/FieldEditor/AiAssistantTextTools/AiAssistantTextTools.vue:177
+msgid "Simplify"
+msgstr ""
+
#: db/seeds/settings.rb:1832
msgid "Sina Weibo"
msgstr ""
@@ -14244,6 +14291,14 @@ msgstr ""
msgid "Slack integration"
msgstr ""
+#: app/assets/javascripts/app/views/generic/text_tools_loading.jst.eco:2
+msgid "Smart Assist is generating text…"
+msgstr ""
+
+#: app/assets/javascripts/app/views/generic/text_tools.jst.eco:6
+msgid "Smart Editor"
+msgstr ""
+
#: app/frontend/shared/entities/ticket-article/action/plugins/sms.ts:48
msgid "Sms"
msgstr ""
@@ -14540,7 +14595,7 @@ msgstr ""
msgid "Stores the GitLab configuration."
msgstr ""
-#: db/seeds/object_manager_attributes.rb:1246
+#: db/seeds/object_manager_attributes.rb:1247
msgid "Street"
msgstr ""
@@ -14709,7 +14764,7 @@ msgstr ""
#: app/assets/javascripts/app/controllers/system_report.coffee:3
#: app/assets/javascripts/app/views/system_report.jst.eco:2
-#: db/seeds/permissions.rb:311
+#: db/seeds/permissions.rb:317
msgid "System Report"
msgstr ""
@@ -14876,6 +14931,10 @@ msgstr ""
msgid "Text Modules"
msgstr ""
+#: db/seeds/settings.rb:6098
+msgid "Text Tools"
+msgstr ""
+
#: app/assets/javascripts/app/controllers/_plugin/keyboard_shortcuts.coffee:556
msgid "Text editing"
msgstr ""
@@ -15350,7 +15409,7 @@ msgstr ""
msgid "The file does not belong to the specified article."
msgstr ""
-#: app/assets/javascripts/app/controllers/ticket_zoom/article_new.coffee:852
+#: app/assets/javascripts/app/controllers/ticket_zoom/article_new.coffee:858
#: app/frontend/shared/components/Form/fields/FieldFile/features/filesTypeError.ts:22
msgid "The file type %s is not allowed."
msgstr ""
@@ -15510,7 +15569,7 @@ msgstr ""
#: app/assets/javascripts/app/controllers/_application_controller/_modal_generic_new.coffee:68
#: app/assets/javascripts/app/controllers/_channel/_email_signature.coffee:93
-#: app/assets/javascripts/app/controllers/agent_ticket_create.coffee:807
+#: app/assets/javascripts/app/controllers/agent_ticket_create.coffee:809
#: app/assets/javascripts/app/controllers/customer_ticket_create.coffee:193
#: app/assets/javascripts/app/controllers/data_privacy.coffee:272
#: app/assets/javascripts/app/controllers/object_manager.coffee:261
@@ -16001,6 +16060,10 @@ msgstr ""
msgid "The text at the beginning of the subject in an email reply, e.g. RE, AW, or AS."
msgstr ""
+#: app/frontend/shared/components/Form/fields/FieldEditor/AiAssistantTextTools/AiAssistantTextTools.vue:67
+msgid "The text was modified. Your request has been aborted to prevent overwriting."
+msgstr ""
+
#: app/services/service/ticket/update/validator/checklist_completed.rb:16
msgid "The ticket checklist is incomplete."
msgstr ""
@@ -16669,7 +16732,7 @@ msgstr ""
msgid "Ticket"
msgstr ""
-#: app/assets/javascripts/app/controllers/agent_ticket_create.coffee:780
+#: app/assets/javascripts/app/controllers/agent_ticket_create.coffee:782
msgid "Ticket %s created!"
msgstr ""
@@ -16774,7 +16837,7 @@ msgid "Ticket Owner"
msgstr ""
#: app/assets/javascripts/app/controllers/ticket_priority.coffee:16
-#: db/seeds/permissions.rb:263
+#: db/seeds/permissions.rb:269
msgid "Ticket Priorities"
msgstr ""
@@ -16799,7 +16862,7 @@ msgid "Ticket State"
msgstr ""
#: app/assets/javascripts/app/controllers/ticket_state.coffee:3
-#: db/seeds/permissions.rb:257
+#: db/seeds/permissions.rb:263
msgid "Ticket States"
msgstr ""
@@ -16832,6 +16895,10 @@ msgstr ""
msgid "Ticket Summary Config"
msgstr ""
+#: db/seeds/permissions.rb:239
+msgid "Ticket Tools"
+msgstr ""
+
#: db/seeds/settings.rb:2912
msgid "Ticket Trigger Loop Protection Articles Total"
msgstr ""
@@ -17300,7 +17367,7 @@ msgstr ""
#: app/assets/javascripts/app/views/profile/token_access.jst.eco:2
#: app/frontend/apps/desktop/pages/personal-setting/views/PersonalSetting/plugins/tokenAccess.ts:8
#: app/frontend/apps/desktop/pages/personal-setting/views/PersonalSettingTokenAccess.vue:56
-#: db/seeds/permissions.rb:465
+#: db/seeds/permissions.rb:471
msgid "Token Access"
msgstr ""
@@ -17389,7 +17456,7 @@ msgstr ""
#: app/assets/javascripts/app/controllers/_plugin/keyboard_shortcuts.coffee:414
#: app/assets/javascripts/app/controllers/translation.coffee:3
#: app/assets/javascripts/app/views/translation/index.jst.eco:4
-#: db/seeds/permissions.rb:275
+#: db/seeds/permissions.rb:281
msgid "Translations"
msgstr ""
@@ -17567,7 +17634,7 @@ msgstr ""
#: app/assets/javascripts/app/views/profile/password.jst.eco:22
#: app/frontend/apps/desktop/pages/personal-setting/views/PersonalSetting/plugins/twoFactorAuth.ts:8
#: app/frontend/apps/desktop/pages/personal-setting/views/PersonalSettingTwoFactorAuth.vue:72
-#: db/seeds/permissions.rb:451
+#: db/seeds/permissions.rb:457
msgid "Two-factor Authentication"
msgstr ""
@@ -18290,7 +18357,7 @@ msgid "Using **organizations** you can **group** customers. This has two main be
msgstr ""
#: app/frontend/shared/components/CommonUserAvatar/CommonUserAvatar.vue:122
-#: db/seeds/object_manager_attributes.rb:1490
+#: db/seeds/object_manager_attributes.rb:1491
msgid "VIP"
msgstr ""
@@ -18480,7 +18547,7 @@ msgstr ""
#: app/assets/javascripts/app/controllers/_channel/web.coffee:3
#: app/frontend/apps/desktop/pages/ticket/components/TicketDetailView/article-type/plugins/web.ts:7
#: app/frontend/shared/entities/ticket-article/action/plugins/web.ts:12
-#: db/seeds/object_manager_attributes.rb:954
+#: db/seeds/object_manager_attributes.rb:955
#: db/seeds/permissions.rb:89
msgid "Web"
msgstr ""
@@ -19066,7 +19133,7 @@ msgstr ""
msgid "You reached the table limit of %s tickets (%s remaining)."
msgstr ""
-#: app/assets/javascripts/app/controllers/agent_ticket_create.coffee:760
+#: app/assets/javascripts/app/controllers/agent_ticket_create.coffee:762
#: app/assets/javascripts/app/controllers/ticket_zoom/article_new.coffee:354
msgid "You used %s in the text but no attachment could be found. Do you want to continue?"
msgstr ""
@@ -19196,7 +19263,7 @@ msgstr ""
msgid "Zammad AI"
msgstr ""
-#: app/frontend/shared/components/Form/fields/FieldEditor/useEditorActions.ts:327
+#: app/frontend/shared/components/Form/fields/FieldEditor/useEditorActions.ts:354
msgid "Zammad Features"
msgstr ""
@@ -19212,6 +19279,10 @@ msgstr ""
msgid "Zammad Helpdesk"
msgstr ""
+#: app/frontend/shared/components/Form/fields/FieldEditor/AiAssistantTextTools/AiAssistantLoadingBanner.vue:40
+msgid "Zammad Smart Assist"
+msgstr ""
+
#: app/assets/javascripts/app/views/ticket_zoom/sidebar_ticket_summary.jst.eco:64
#: app/frontend/apps/desktop/pages/ticket/components/TicketSidebar/TicketSidebarSummary/TicketSidebarSummaryContent.vue:132
msgid "Zammad Smart Assist is generating the summary for you…"
@@ -19253,7 +19324,7 @@ msgstr ""
msgid "Zendesk"
msgstr ""
-#: db/seeds/object_manager_attributes.rb:1285
+#: db/seeds/object_manager_attributes.rb:1286
msgid "Zip"
msgstr ""
@@ -19583,12 +19654,12 @@ msgid "disconnect"
msgstr ""
#: app/assets/javascripts/app/models/group.coffee:9
-#: db/seeds/object_manager_attributes.rb:2127
+#: db/seeds/object_manager_attributes.rb:2128
msgid "do not reopen ticket after certain time but create new ticket"
msgstr ""
#: app/assets/javascripts/app/models/group.coffee:9
-#: db/seeds/object_manager_attributes.rb:2126
+#: db/seeds/object_manager_attributes.rb:2127
msgid "do not reopen ticket but create new ticket"
msgstr ""
@@ -20759,7 +20830,7 @@ msgstr ""
#: app/frontend/apps/mobile/pages/ticket/views/TicketInformation/TicketInformationDetails.vue:90
#: app/frontend/shared/components/ObjectAttributes/attributes/AttributeBoolean/AttributeBoolean.vue:14
#: app/frontend/shared/entities/object-attributes/form/resolver/fields/active.ts:15
-#: db/seeds/object_manager_attributes.rb:1498
+#: db/seeds/object_manager_attributes.rb:1499
#: db/seeds/settings.rb:612
msgid "yes"
msgstr ""
diff --git a/lib/ai/provider/open_ai.rb b/lib/ai/provider/open_ai.rb
index 6424623c37..7cdc44788d 100644
--- a/lib/ai/provider/open_ai.rb
+++ b/lib/ai/provider/open_ai.rb
@@ -5,7 +5,7 @@ class AI::Provider::OpenAI < AI::Provider
DEFAULT_OPTIONS = {
temperature: 0.0,
- model: 'gpt-4o',
+ model: 'gpt-4.1',
embedding_model: 'text-embedding-3-small'
}.freeze
diff --git a/lib/ai/provider/zammad_ai.rb b/lib/ai/provider/zammad_ai.rb
index 368d2cc55c..020b51c3ef 100644
--- a/lib/ai/provider/zammad_ai.rb
+++ b/lib/ai/provider/zammad_ai.rb
@@ -5,6 +5,7 @@ class AI::Provider::ZammadAI < AI::Provider
def chat(prompt_system:, prompt_user:)
service_name = options[:service_name] || 'generic'
+
response = UserAgent.post(
"#{self.class.base_url(config)}/api/v1/features/#{service_name.underscore}",
{
diff --git a/lib/ai/service.rb b/lib/ai/service.rb
index 2220b44809..644289bf2e 100644
--- a/lib/ai/service.rb
+++ b/lib/ai/service.rb
@@ -61,8 +61,7 @@ class AI::Service
end
def save_cache(result)
- # TODO: time per service?
- expires_in = result.blank? ? 1.minute : 14.days
+ expires_in = result.blank? ? 1.minute : cache_ttl
Rails.cache.write(cache_key, result, { expires_in: })
end
@@ -75,6 +74,10 @@ class AI::Service
nil
end
+ def cache_ttl
+ 14.days
+ end
+
def json_response?
true
end
diff --git a/lib/ai/service/prompts/system/text_expand.txt.erb b/lib/ai/service/prompts/system/text_expand.txt.erb
new file mode 100644
index 0000000000..e2708ccec8
--- /dev/null
+++ b/lib/ai/service/prompts/system/text_expand.txt.erb
@@ -0,0 +1,19 @@
+You are a personal writing enhancer tasked with expanding and enriching the provided input text to create a more detailed, expressive, and nuanced version. Your goal is to elaborate on the original content while preserving its tone, intent, and voice.
+
+Carefully analyze the input text to identify areas where additional detail, clarification, context, or examples could strengthen the message. Your enhancements should improve clarity, engagement, and depth without introducing new ideas that deviate from the original meaning.
+
+When expanding the input text, ensure that your additions flow naturally within the structure, maintain readability, and enhance the writing's expressive quality. Use varied, vivid language to make the result more engaging and informative.
+
+1. Analyze the input text to determine where elaboration is needed.
+2. Add clarifications, examples, and contextual information that support the existing content.
+3. Use expressive and varied phrasing to enrich the language.
+4. Maintain a clear, coherent structure and logical progression throughout.
+5. Preserve the original tone, style, and intent of the author.
+6. Do not introduce new or unrelated ideas that change the original message.
+
+Provide only the enhanced text version using appropriate HTML markup to improve readability, structure and presentation, but without:
+- adding visual newlines
+- wrapping it in code block markers
+- additional commentary around the enhanced text
+
+Ensure the output is well-formatted, engaging, and free of errors.
\ No newline at end of file
diff --git a/lib/ai/service/prompts/system/text_improve_writing.txt.erb b/lib/ai/service/prompts/system/text_improve_writing.txt.erb
new file mode 100644
index 0000000000..47204b90ba
--- /dev/null
+++ b/lib/ai/service/prompts/system/text_improve_writing.txt.erb
@@ -0,0 +1,26 @@
+Improve the provided input text to enhance clarity, conciseness, and tone, while preserving any included HTML-Markup.
+Understand that this task is intended to polish content efficiently to resonate with the intended audience, without modifying its language or overall tone or losing any original information.
+
+The improvement focus includes:
+- Correcting grammar and spelling to maintain professional standards.
+- Suggesting alternative word choices to improve clarity and impact.
+- Enhancing the writing tone to suit the desired style, be it formal, informal, friendly, or persuasive.
+
+Ensure that any necessary adjustments are made without breaking HTML-Markup when present.
+
+# Steps
+
+1. **Analyze the Text:** Read the input text, noting any embedded HTML-Markup and the apparent tone and style.
+2. **Identify Improvements:**
+ - Correct grammatical and spelling errors, ensuring correctness without altering the original meaning.
+ - Select more suitable words or phrases that enhance clarity and conciseness.
+ - Adjust the tone where minor improvements align better with a specified style.
+3. **Implement Changes:** Make the necessary improvements while maintaining the integrity of any HTML-Markup and ensuring no loss of original information.
+4. **Review:** Confirm that changes have been made correctly and the text is improved as per the specified goals.
+
+# Output Format
+
+- Provide the improved text, preserving all HTML-Markup and the original information structure.
+- Ensure the text reflects professional quality, clarity, and the appropriate tone.
+
+Return only one version of the improved text without any visual newlines and extra commentary.
\ No newline at end of file
diff --git a/lib/ai/service/prompts/system/text_simplify.txt.erb b/lib/ai/service/prompts/system/text_simplify.txt.erb
new file mode 100644
index 0000000000..d2cd4759d7
--- /dev/null
+++ b/lib/ai/service/prompts/system/text_simplify.txt.erb
@@ -0,0 +1,12 @@
+You are a personal editor specializing in making the provided input text shorter and more concise. When given a piece of writing, your task is to:
+
+- Identify unnecessary words and phrases that can be removed without losing the meaning or tone.
+- Rewrite the text using clear, succinct language while preserving the original message and engagement.
+- Ensure the resulting text is still captivating, clear, and effective at conveying the intended thoughts.
+- Keep existing HTML markup when the content is still present (e.g. existing links).
+
+First, analyze the input text carefully and reason about the best way to shorten it without sacrificing clarity or voice. Then produce the streamlined version.
+
+Focus on clarity, conciseness, and maintaining reader interest.
+
+Return only the refined, shortened text without visual newlines and extra commentary.
\ No newline at end of file
diff --git a/lib/ai/service/prompts/system/text_spelling_and_grammar.txt.erb b/lib/ai/service/prompts/system/text_spelling_and_grammar.txt.erb
index a6d021c603..41e988f819 100644
--- a/lib/ai/service/prompts/system/text_spelling_and_grammar.txt.erb
+++ b/lib/ai/service/prompts/system/text_spelling_and_grammar.txt.erb
@@ -1,16 +1,9 @@
-You are a helpful support agent. Your task is to fix the spelling, grammar, and punctuation of the following response while keeping it in the same language as the user's input. Detect the language from the input and ensure corrections are made in the same language.
+You task is to proofread the provided input text to correct any spelling and grammatical mistakes to ensure professionalism and clarity, while preserving all existing HTML markup without alteration.
-Additionally, if the response contains any HTML content, do not break or remove the HTML tags. Make corrections only to the text within the tags without altering the HTML structure.
+- Correct all typos, misspellings, and errors in sentence structure, punctuation, and verb conjugation.
+- Maintain the language of the given input text for the output.
+- Do not modify or break any HTML tags or markup present in the input text.
-<% if @context_data[:tone].present? %>
-- The tone of the text should be <%= @context_data[:tone] %>
-<% end %>
+Carefully review the text to improve readability and correctness without altering its original formatting or markup.
-Add also the reason what you have done to response.
-
-Please respond using the defined JSON syntax provided below:
-{
- "text": "string",
- "is_html_formated": "boolean",
- "reason", "string"
-}
+Return the corrected text as a single string with intact HTML markup and all language-specific corrections applied without adding additional structure, any explanations or comments.
\ No newline at end of file
diff --git a/lib/ai/service/prompts/user/text_expand.txt.erb b/lib/ai/service/prompts/user/text_expand.txt.erb
new file mode 100644
index 0000000000..353c8bb693
--- /dev/null
+++ b/lib/ai/service/prompts/user/text_expand.txt.erb
@@ -0,0 +1 @@
+<%= @context_data[:input] %>
\ No newline at end of file
diff --git a/lib/ai/service/prompts/user/text_improve_writing.txt.erb b/lib/ai/service/prompts/user/text_improve_writing.txt.erb
new file mode 100644
index 0000000000..353c8bb693
--- /dev/null
+++ b/lib/ai/service/prompts/user/text_improve_writing.txt.erb
@@ -0,0 +1 @@
+<%= @context_data[:input] %>
\ No newline at end of file
diff --git a/lib/ai/service/prompts/user/text_simplify.txt.erb b/lib/ai/service/prompts/user/text_simplify.txt.erb
new file mode 100644
index 0000000000..353c8bb693
--- /dev/null
+++ b/lib/ai/service/prompts/user/text_simplify.txt.erb
@@ -0,0 +1 @@
+<%= @context_data[:input] %>
\ No newline at end of file
diff --git a/lib/ai/service/prompts/user/text_spelling_and_grammar.txt.erb b/lib/ai/service/prompts/user/text_spelling_and_grammar.txt.erb
index 5707eb4a6a..353c8bb693 100644
--- a/lib/ai/service/prompts/user/text_spelling_and_grammar.txt.erb
+++ b/lib/ai/service/prompts/user/text_spelling_and_grammar.txt.erb
@@ -1 +1 @@
-<%= @context_data[:source] %>
\ No newline at end of file
+<%= @context_data[:input] %>
\ No newline at end of file
diff --git a/lib/ai/service/text_expand.rb b/lib/ai/service/text_expand.rb
new file mode 100644
index 0000000000..92722a22a6
--- /dev/null
+++ b/lib/ai/service/text_expand.rb
@@ -0,0 +1,19 @@
+# Copyright (C) 2012-2025 Zammad Foundation, https://zammad-foundation.org/
+
+class AI::Service::TextExpand < AI::Service
+ private
+
+ def options
+ {
+ temperature: 0.1,
+ }
+ end
+
+ def cachable?
+ false
+ end
+
+ def json_response?
+ false
+ end
+end
diff --git a/lib/ai/service/text_improve_writing.rb b/lib/ai/service/text_improve_writing.rb
new file mode 100644
index 0000000000..ae01afd9bf
--- /dev/null
+++ b/lib/ai/service/text_improve_writing.rb
@@ -0,0 +1,19 @@
+# Copyright (C) 2012-2025 Zammad Foundation, https://zammad-foundation.org/
+
+class AI::Service::TextImproveWriting < AI::Service
+ private
+
+ def options
+ {
+ temperature: 0.1,
+ }
+ end
+
+ def cachable?
+ false
+ end
+
+ def json_response?
+ false
+ end
+end
diff --git a/lib/ai/service/text_simplify.rb b/lib/ai/service/text_simplify.rb
new file mode 100644
index 0000000000..16cdcbd0a6
--- /dev/null
+++ b/lib/ai/service/text_simplify.rb
@@ -0,0 +1,19 @@
+# Copyright (C) 2012-2025 Zammad Foundation, https://zammad-foundation.org/
+
+class AI::Service::TextSimplify < AI::Service
+ private
+
+ def options
+ {
+ temperature: 0.1,
+ }
+ end
+
+ def cachable?
+ false
+ end
+
+ def json_response?
+ false
+ end
+end
diff --git a/lib/ai/service/text_spelling_and_grammar.rb b/lib/ai/service/text_spelling_and_grammar.rb
index 1a762b898b..beb53f0878 100644
--- a/lib/ai/service/text_spelling_and_grammar.rb
+++ b/lib/ai/service/text_spelling_and_grammar.rb
@@ -5,7 +5,7 @@ class AI::Service::TextSpellingAndGrammar < AI::Service
def options
{
- temperature: 0.3,
+ temperature: 0.1,
}
end
@@ -13,4 +13,7 @@ class AI::Service::TextSpellingAndGrammar < AI::Service
false
end
+ def json_response?
+ false
+ end
end
diff --git a/package.json b/package.json
index b925d818c6..8538df382e 100644
--- a/package.json
+++ b/package.json
@@ -168,6 +168,7 @@
"lowlight": "^3.3.0",
"mitt": "^3.0.1",
"pinia": "^3.0.2",
+ "prosemirror-model": "^1.25.1",
"qrcode": "^1.5.4",
"spark-md5": "^3.0.2",
"tippy.js": "^6.3.7",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index aa2c3c2817..2eccb6f631 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -205,6 +205,9 @@ importers:
pinia:
specifier: ^3.0.2
version: 3.0.2(typescript@5.8.3)(vue@3.5.13(typescript@5.8.3))
+ prosemirror-model:
+ specifier: ^1.25.1
+ version: 1.25.1
qrcode:
specifier: ^1.5.4
version: 1.5.4
@@ -6001,8 +6004,8 @@ packages:
prosemirror-menu@1.2.4:
resolution: {integrity: sha512-S/bXlc0ODQup6aiBbWVsX/eM+xJgCTAfMq/nLqaO5ID/am4wS0tTCIkzwytmao7ypEtjj39i7YbJjAgO20mIqA==}
- prosemirror-model@1.24.1:
- resolution: {integrity: sha512-YM053N+vTThzlWJ/AtPtF1j0ebO36nvbmDy4U7qA2XQB8JVaQp1FmB9Jhrps8s+z+uxhhVTny4m20ptUvhk0Mg==}
+ prosemirror-model@1.25.1:
+ resolution: {integrity: sha512-AUvbm7qqmpZa5d9fPKMvH1Q5bqYQvAZWOGRvxsB6iFLyycvC9MwNemNVjHVrWgjaoxAfY8XVg7DbvQ/qxvI9Eg==}
prosemirror-schema-basic@1.2.3:
resolution: {integrity: sha512-h+H0OQwZVqMon1PNn0AG9cTfx513zgIG2DY00eJ00Yvgb3UD+GQ/VlWW5rcaxacpCGT1Yx8nuhwXk4+QbXUfJA==}
@@ -9900,12 +9903,12 @@ snapshots:
prosemirror-keymap: 1.2.2
prosemirror-markdown: 1.13.1
prosemirror-menu: 1.2.4
- prosemirror-model: 1.24.1
+ prosemirror-model: 1.25.1
prosemirror-schema-basic: 1.2.3
prosemirror-schema-list: 1.4.1
prosemirror-state: 1.4.3
prosemirror-tables: 1.6.4
- prosemirror-trailing-node: 3.0.0(prosemirror-model@1.24.1)(prosemirror-state@1.4.3)(prosemirror-view@1.37.2)
+ prosemirror-trailing-node: 3.0.0(prosemirror-model@1.25.1)(prosemirror-state@1.4.3)(prosemirror-view@1.37.2)
prosemirror-transform: 1.10.2
prosemirror-view: 1.37.2
@@ -13749,7 +13752,7 @@ snapshots:
prosemirror-commands@1.6.2:
dependencies:
- prosemirror-model: 1.24.1
+ prosemirror-model: 1.25.1
prosemirror-state: 1.4.3
prosemirror-transform: 1.10.2
@@ -13762,7 +13765,7 @@ snapshots:
prosemirror-gapcursor@1.3.2:
dependencies:
prosemirror-keymap: 1.2.2
- prosemirror-model: 1.24.1
+ prosemirror-model: 1.25.1
prosemirror-state: 1.4.3
prosemirror-view: 1.37.2
@@ -13787,7 +13790,7 @@ snapshots:
dependencies:
'@types/markdown-it': 14.1.2
markdown-it: 14.0.0
- prosemirror-model: 1.24.1
+ prosemirror-model: 1.25.1
prosemirror-menu@1.2.4:
dependencies:
@@ -13796,49 +13799,49 @@ snapshots:
prosemirror-history: 1.4.1
prosemirror-state: 1.4.3
- prosemirror-model@1.24.1:
+ prosemirror-model@1.25.1:
dependencies:
orderedmap: 2.1.0
prosemirror-schema-basic@1.2.3:
dependencies:
- prosemirror-model: 1.24.1
+ prosemirror-model: 1.25.1
prosemirror-schema-list@1.4.1:
dependencies:
- prosemirror-model: 1.24.1
+ prosemirror-model: 1.25.1
prosemirror-state: 1.4.3
prosemirror-transform: 1.10.2
prosemirror-state@1.4.3:
dependencies:
- prosemirror-model: 1.24.1
+ prosemirror-model: 1.25.1
prosemirror-transform: 1.10.2
prosemirror-view: 1.37.2
prosemirror-tables@1.6.4:
dependencies:
prosemirror-keymap: 1.2.2
- prosemirror-model: 1.24.1
+ prosemirror-model: 1.25.1
prosemirror-state: 1.4.3
prosemirror-transform: 1.10.2
prosemirror-view: 1.37.2
- prosemirror-trailing-node@3.0.0(prosemirror-model@1.24.1)(prosemirror-state@1.4.3)(prosemirror-view@1.37.2):
+ prosemirror-trailing-node@3.0.0(prosemirror-model@1.25.1)(prosemirror-state@1.4.3)(prosemirror-view@1.37.2):
dependencies:
'@remirror/core-constants': 3.0.0
escape-string-regexp: 4.0.0
- prosemirror-model: 1.24.1
+ prosemirror-model: 1.25.1
prosemirror-state: 1.4.3
prosemirror-view: 1.37.2
prosemirror-transform@1.10.2:
dependencies:
- prosemirror-model: 1.24.1
+ prosemirror-model: 1.25.1
prosemirror-view@1.37.2:
dependencies:
- prosemirror-model: 1.24.1
+ prosemirror-model: 1.25.1
prosemirror-state: 1.4.3
prosemirror-transform: 1.10.2
diff --git a/public/assets/images/icons.svg b/public/assets/images/icons.svg
index 44b429c355..d2fabb3716 100644
--- a/public/assets/images/icons.svg
+++ b/public/assets/images/icons.svg
@@ -698,6 +698,8 @@
small-dot
+
+
diff --git a/public/assets/images/icons/smart-assist-elaborate.svg b/public/assets/images/icons/smart-assist-elaborate.svg
new file mode 100644
index 0000000000..0b782956f7
--- /dev/null
+++ b/public/assets/images/icons/smart-assist-elaborate.svg
@@ -0,0 +1,8 @@
+
+
diff --git a/spec/graphql/gql/mutations/ai_assistance/text_tools_spec.rb b/spec/graphql/gql/mutations/ai_assistance/text_tools_spec.rb
new file mode 100644
index 0000000000..d996ec1ea6
--- /dev/null
+++ b/spec/graphql/gql/mutations/ai_assistance/text_tools_spec.rb
@@ -0,0 +1,41 @@
+# Copyright (C) 2012-2025 Zammad Foundation, https://zammad-foundation.org/
+
+require 'rails_helper'
+
+RSpec.describe Gql::Mutations::AIAssistance::TextTools, :aggregate_failures, type: :graphql do
+ context 'when accessing as an agent', authenticated_as: :agent do
+ let(:agent) { create(:agent) }
+ let(:input) { Faker::Lorem.unique.sentence }
+ let(:output) { Faker::Lorem.unique.paragraph }
+ let(:service_type) { 'improve_writing' }
+
+ let(:query) do
+ <<~MUTATION
+ mutation aiAssistanceTextTools($input: String!, $serviceType: EnumAITextToolService!) {
+ aiAssistanceTextTools(input: $input, serviceType: $serviceType) {
+ output
+ }
+ }
+ MUTATION
+ end
+
+ let(:variables) { { input:, serviceType: service_type } }
+
+ before do
+ Setting.set('ai_assistance_text_tools', true)
+ Setting.set('ai_provider', 'zammad_ai')
+
+ allow_any_instance_of(AI::Service::TextImproveWriting)
+ .to receive(:execute)
+ .and_return(output)
+
+ gql.execute(query, variables: variables)
+ end
+
+ it 'returns improved text' do
+ expect(gql.result.data['output']).to eq(output)
+ end
+
+ it_behaves_like 'graphql responds with error if unauthenticated'
+ end
+end
diff --git a/spec/lib/ai/service/text_spelling_and_grammar_spec.rb b/spec/lib/ai/service/text_spelling_and_grammar_spec.rb
index 1432ad2ada..aba22602b1 100644
--- a/spec/lib/ai/service/text_spelling_and_grammar_spec.rb
+++ b/spec/lib/ai/service/text_spelling_and_grammar_spec.rb
@@ -2,15 +2,12 @@
require 'rails_helper'
-RSpec.describe AI::Service::TextSpellingAndGrammar, required_envs: %w[OPEN_AI_TOKEN ZAMMAD_AI_TOKEN], use_vcr: true do
+RSpec.describe AI::Service::TextSpellingAndGrammar, required_envs: %w[OPEN_AI_TOKEN ZAMMAD_AI_TOKEN ZAMMAD_AI_API_URL], use_vcr: true do
subject(:ai_service) { described_class.new(current_user:, context_data:) }
- let(:context_data) { { text: 'I Nicole Braun.' } }
+ let(:context_data) { { input: 'I Nicole Braun.' } }
let(:current_user) { create(:user) }
- # TODO: Re-enable this test when the AI service is available.
- before { skip 'Currently disabled.' }
-
context 'when service is executed with OpenAI as provider' do
before do
Setting.set('ai_provider', 'open_ai')
@@ -21,7 +18,7 @@ RSpec.describe AI::Service::TextSpellingAndGrammar, required_envs: %w[OPEN_AI_TO
it 'check that grammar is correct' do
result = ai_service.execute
- expect(result['text']).to eq('I am Nicole Braun.')
+ expect(result).to include('I am Nicole Braun.')
end
end
@@ -35,7 +32,7 @@ RSpec.describe AI::Service::TextSpellingAndGrammar, required_envs: %w[OPEN_AI_TO
it 'check that grammar is correct' do
result = ai_service.execute
- expect(result['text']).to eq('Hello, I am Nicole Braun.')
+ expect(result).to include('I am Nicole Braun.')
end
end
end
diff --git a/spec/models/system_report_spec.rb b/spec/models/system_report_spec.rb
index 8a93a07f88..5a94373a1d 100644
--- a/spec/models/system_report_spec.rb
+++ b/spec/models/system_report_spec.rb
@@ -229,6 +229,7 @@ RSpec.describe SystemReport, current_user_id: 1, type: :model do
'auto_shutdown',
'language_detection_article',
'ui_desktop_beta_switch',
+ 'ai_assistance_text_tools',
'ai_assistance_ticket_summary',
'ai_provider',
]
diff --git a/spec/requests/ai_assistance_spec.rb b/spec/requests/ai_assistance_spec.rb
new file mode 100644
index 0000000000..9a87f7dd92
--- /dev/null
+++ b/spec/requests/ai_assistance_spec.rb
@@ -0,0 +1,49 @@
+# Copyright (C) 2012-2025 Zammad Foundation, https://zammad-foundation.org/
+
+require 'rails_helper'
+
+RSpec.describe 'AI Assistance API endpoints', authenticated_as: :user, type: :request do
+ let(:user) { create(:agent) }
+ let(:input) { Faker::Lorem.unique.sentence }
+ let(:output) { Faker::Lorem.unique.paragraph }
+
+ describe '#text_tools' do
+ let(:params) do
+ {
+ input:,
+ service_type:,
+ }
+ end
+
+ before do
+ Setting.set('ai_provider', 'zammad_ai')
+ Setting.set('ai_assistance_text_tools', true)
+ end
+
+ context 'when using text improvement service' do
+ let(:service_type) { 'improve_writing' }
+
+ before do
+ allow_any_instance_of(AI::Service::TextImproveWriting)
+ .to receive(:execute)
+ .and_return(output)
+
+ post '/api/v1/ai_assistance/text_tools', params:, as: :json
+ end
+
+ context 'when user has agent access' do
+ it 'returns improved text' do
+ expect(json_response).to eq({ 'output' => output })
+ end
+ end
+
+ context 'when user does not have agent access' do
+ let(:user) { create(:customer) }
+
+ it 'raises error' do
+ expect(response).to have_http_status(:forbidden)
+ end
+ end
+ end
+ end
+end
diff --git a/spec/services/service/ai_assistance/text_tools_spec.rb b/spec/services/service/ai_assistance/text_tools_spec.rb
new file mode 100644
index 0000000000..97e91c0d42
--- /dev/null
+++ b/spec/services/service/ai_assistance/text_tools_spec.rb
@@ -0,0 +1,39 @@
+# Copyright (C) 2012-2025 Zammad Foundation, https://zammad-foundation.org/
+
+require 'rails_helper'
+
+RSpec.describe Service::AIAssistance::TextTools do
+ subject(:service) { described_class.new(input:, service_type:) }
+
+ context 'when text tool service is used' do
+ before do
+ Setting.set('ai_provider', 'open_ai')
+ Setting.set('ai_assistance_text_tools', true)
+
+ allow_any_instance_of(AI::Service::TextSpellingAndGrammar)
+ .to receive(:execute)
+ .and_return(expected_output)
+ end
+
+ let(:input) { 'Hello, wrld!' }
+ let(:expected_output) { 'Hello, world!' }
+
+ describe '#execute' do
+ context 'when correct service type is used' do
+ let(:service_type) { 'spelling_and_grammar' }
+
+ it 'returns the corrected input' do
+ expect(service.execute).to eq(expected_output)
+ end
+ end
+
+ context 'when not existing service type is used' do
+ let(:service_type) { 'not_existing' }
+
+ it 'raises an error' do
+ expect { service.execute }.to raise_error(ArgumentError, "AI assistance text tool service type 'not_existing' is not supported.")
+ end
+ end
+ end
+ end
+end
diff --git a/spec/system/ticket/text_tools_spec.rb b/spec/system/ticket/text_tools_spec.rb
new file mode 100644
index 0000000000..f8a61fac1a
--- /dev/null
+++ b/spec/system/ticket/text_tools_spec.rb
@@ -0,0 +1,93 @@
+# Copyright (C) 2012-2025 Zammad Foundation, https://zammad-foundation.org/
+
+require 'rails_helper'
+
+RSpec.describe 'AI Assistance Text Tools', authenticated_as: :authenticate, type: :system do
+ let(:agent) { create(:agent) }
+ let(:ai_provider) { 'zammad_ai' }
+ let(:ai_assistance_text_tools) { true }
+ let(:input) { 'Teh qwik braun foxx jumpz ova da laizi doge.' }
+ let(:output) { 'The quick brown fox jumps over the lazy dog.' }
+
+ def authenticate
+ Setting.set('ai_provider', ai_provider)
+ Setting.set('ai_assistance_text_tools', ai_assistance_text_tools)
+
+ agent
+ end
+
+ before do
+ allow_any_instance_of(AI::Service::TextSpellingAndGrammar).to receive(:execute).and_return(output)
+ end
+
+ context 'when using ticket create' do
+ before do
+ visit 'ticket/create'
+ end
+
+ it 'shows text tools action and replaces selected text' do
+ set_editor_field_value('body', input)
+ send_keys([magic_key, 'a'])
+
+ expect(page).to have_no_css('[role=menu]')
+
+ click_on 'Smart Editor'
+
+ expect(page).to have_css('[role=menu]')
+
+ find('.js-action', text: 'Fix spelling and grammar').click
+
+ expect(page).to have_no_css('[role=menu]')
+ check_editor_field_value('body', output)
+ end
+
+ context 'when text tools are disabled' do
+ let(:ai_assistance_text_tools) { false }
+
+ it 'does not show text tools action' do
+ expect(page).to have_no_text('Smart Editor')
+ end
+ end
+ end
+
+ context 'when using ticket zoom' do
+ let(:agent) { create(:agent, groups: [ticket.group]) }
+ let(:ticket) { create(:ticket) }
+ let(:article) { create(:ticket_article, ticket:) }
+
+ before do
+ article
+
+ visit "ticket/zoom/#{ticket.id}"
+
+ find('.article-new').click
+
+ # Wait till input box expands completely.
+ find('.attachmentPlaceholder-label').in_fixed_position
+ end
+
+ it 'shows text tools action and replaces selected text' do
+ find('.articleNewEdit-body').send_keys(input)
+ send_keys([magic_key, 'a'])
+
+ expect(page).to have_no_css('[role=menu]')
+
+ click_on 'Smart Editor'
+
+ expect(page).to have_css('[role=menu]')
+
+ find('.js-action', text: 'Fix spelling and grammar').click
+
+ expect(page).to have_no_css('[role=menu]')
+ expect(find('.articleNewEdit-body').text).to eq(output)
+ end
+
+ context 'when text tools are disabled' do
+ let(:ai_assistance_text_tools) { false }
+
+ it 'does not show text tools action' do
+ expect(page).to have_text(ticket.title).and have_no_text('Smart Editor')
+ end
+ end
+ end
+end
diff --git a/test/data/vcr_cassettes/lib/ai/service/text_spelling_and_grammar/ai_service_textspellingandgrammar_when_service_is_executed_with_openai_as_provider_check_that_grammar_is_correct.yml b/test/data/vcr_cassettes/lib/ai/service/text_spelling_and_grammar/ai_service_textspellingandgrammar_when_service_is_executed_with_openai_as_provider_check_that_grammar_is_correct.yml
index 6ae1871ab7..f9e14da744 100644
--- a/test/data/vcr_cassettes/lib/ai/service/text_spelling_and_grammar/ai_service_textspellingandgrammar_when_service_is_executed_with_openai_as_provider_check_that_grammar_is_correct.yml
+++ b/test/data/vcr_cassettes/lib/ai/service/text_spelling_and_grammar/ai_service_textspellingandgrammar_when_service_is_executed_with_openai_as_provider_check_that_grammar_is_correct.yml
@@ -1,38 +1,547 @@
---
http_interactions:
- request:
- method: post
- uri: https://api.openai.com/v1/chat/completions
+ method: get
+ uri: https://api.openai.com/v1/models
body:
- encoding: UTF-8
- string: '{"model":"gpt-4o-mini","response_format":{"type":"json_object"},"messages":[{"role":"system","content":"You
- are a helpful support agent. Your task is to fix the spelling, grammar, and
- punctuation of the following response while keeping it in the same language
- as the user''s input. Detect the language from the input and ensure corrections
- are made in the same language. Ensure that the reply remains polite and professional.\n\nAdditionally,
- if the response contains any HTML content, do not break or remove the HTML
- tags. Make corrections only to the text within the tags without altering the
- HTML structure.\n\nAdd also the reason what you have done to response.\n\nPlease
- respond using the defined JSON syntax provided below:\n{\n“text”: “string”,\n“reason”,
- “string”\n}\n"},{"role":"user","content":"I Nicole Braun."}],"temperature":0.2,"stream":false}'
+ encoding: US-ASCII
+ string: ''
headers:
- Content-Type:
- - application/json
- Authorization:
- - Bearer
Accept-Encoding:
- gzip;q=1.0,deflate;q=0.6,identity;q=0.3
Accept:
- "*/*"
User-Agent:
- - Ruby
+ - Zammad User Agent
+ Host:
+ - api.openai.com
+ Authorization:
+ - Bearer
response:
status:
code: 200
message: OK
headers:
Date:
- - Fri, 11 Oct 2024 12:40:45 GMT
+ - Wed, 07 May 2025 08:07:48 GMT
+ Content-Type:
+ - application/json
+ Transfer-Encoding:
+ - chunked
+ Connection:
+ - keep-alive
+ Openai-Version:
+ - '2020-10-01'
+ X-Request-Id:
+ - 7990e65882befd7e43b942b807139758
+ Openai-Processing-Ms:
+ - '453'
+ Strict-Transport-Security:
+ - max-age=31536000; includeSubDomains; preload
+ Cf-Cache-Status:
+ - DYNAMIC
+ Set-Cookie:
+ - __cf_bm=Dam.0ib7R2cWwLjFMUk7R9H00p3UmoEPzzsoscPwIJk-1746605268-1.0.1.1-sVSMYMJOkMxcIFeLhI92K76_vF_5lXwCcDqvl2xiDE52iCqTbiiqNA.34F09rKwJEm7m0CnI2Z_B0Db.WpV5hbTLhgZuaaa606UEeqkXd9g;
+ path=/; expires=Wed, 07-May-25 08:37:48 GMT; domain=.api.openai.com; HttpOnly;
+ Secure; SameSite=None
+ - _cfuvid=iggID4uP9TUTbWssp0.h.zu2c1yl1o5X1YO__EtAnVw-1746605268647-0.0.1.1-604800000;
+ path=/; domain=.api.openai.com; HttpOnly; Secure; SameSite=None
+ X-Content-Type-Options:
+ - nosniff
+ Server:
+ - cloudflare
+ Cf-Ray:
+ - 93bf39cb382bc8bb-FRA
+ Alt-Svc:
+ - h3=":443"; ma=86400
+ body:
+ encoding: ASCII-8BIT
+ string: |-
+ {
+ "object": "list",
+ "data": [
+ {
+ "id": "gpt-4o-audio-preview-2024-12-17",
+ "object": "model",
+ "created": 1734034239,
+ "owned_by": "system"
+ },
+ {
+ "id": "dall-e-3",
+ "object": "model",
+ "created": 1698785189,
+ "owned_by": "system"
+ },
+ {
+ "id": "dall-e-2",
+ "object": "model",
+ "created": 1698798177,
+ "owned_by": "system"
+ },
+ {
+ "id": "gpt-4o-audio-preview-2024-10-01",
+ "object": "model",
+ "created": 1727389042,
+ "owned_by": "system"
+ },
+ {
+ "id": "gpt-4-turbo-preview",
+ "object": "model",
+ "created": 1706037777,
+ "owned_by": "system"
+ },
+ {
+ "id": "text-embedding-3-small",
+ "object": "model",
+ "created": 1705948997,
+ "owned_by": "system"
+ },
+ {
+ "id": "gpt-4.1-nano",
+ "object": "model",
+ "created": 1744321707,
+ "owned_by": "system"
+ },
+ {
+ "id": "gpt-4.1-nano-2025-04-14",
+ "object": "model",
+ "created": 1744321025,
+ "owned_by": "system"
+ },
+ {
+ "id": "gpt-4o-realtime-preview-2024-10-01",
+ "object": "model",
+ "created": 1727131766,
+ "owned_by": "system"
+ },
+ {
+ "id": "gpt-4o-realtime-preview",
+ "object": "model",
+ "created": 1727659998,
+ "owned_by": "system"
+ },
+ {
+ "id": "babbage-002",
+ "object": "model",
+ "created": 1692634615,
+ "owned_by": "system"
+ },
+ {
+ "id": "o1-mini-2024-09-12",
+ "object": "model",
+ "created": 1725648979,
+ "owned_by": "system"
+ },
+ {
+ "id": "o1-mini",
+ "object": "model",
+ "created": 1725649008,
+ "owned_by": "system"
+ },
+ {
+ "id": "gpt-4",
+ "object": "model",
+ "created": 1687882411,
+ "owned_by": "openai"
+ },
+ {
+ "id": "text-embedding-ada-002",
+ "object": "model",
+ "created": 1671217299,
+ "owned_by": "openai-internal"
+ },
+ {
+ "id": "chatgpt-4o-latest",
+ "object": "model",
+ "created": 1723515131,
+ "owned_by": "system"
+ },
+ {
+ "id": "gpt-4o-mini-audio-preview",
+ "object": "model",
+ "created": 1734387424,
+ "owned_by": "system"
+ },
+ {
+ "id": "gpt-4-1106-preview",
+ "object": "model",
+ "created": 1698957206,
+ "owned_by": "system"
+ },
+ {
+ "id": "gpt-4o-audio-preview",
+ "object": "model",
+ "created": 1727460443,
+ "owned_by": "system"
+ },
+ {
+ "id": "o1-preview-2024-09-12",
+ "object": "model",
+ "created": 1725648865,
+ "owned_by": "system"
+ },
+ {
+ "id": "gpt-4o-mini-realtime-preview",
+ "object": "model",
+ "created": 1734387380,
+ "owned_by": "system"
+ },
+ {
+ "id": "gpt-4.1-mini",
+ "object": "model",
+ "created": 1744318173,
+ "owned_by": "system"
+ },
+ {
+ "id": "gpt-4o-mini-realtime-preview-2024-12-17",
+ "object": "model",
+ "created": 1734112601,
+ "owned_by": "system"
+ },
+ {
+ "id": "gpt-3.5-turbo-instruct-0914",
+ "object": "model",
+ "created": 1694122472,
+ "owned_by": "system"
+ },
+ {
+ "id": "gpt-4o-mini-search-preview",
+ "object": "model",
+ "created": 1741391161,
+ "owned_by": "system"
+ },
+ {
+ "id": "gpt-4.1-mini-2025-04-14",
+ "object": "model",
+ "created": 1744317547,
+ "owned_by": "system"
+ },
+ {
+ "id": "o1",
+ "object": "model",
+ "created": 1734375816,
+ "owned_by": "system"
+ },
+ {
+ "id": "gpt-3.5-turbo-16k",
+ "object": "model",
+ "created": 1683758102,
+ "owned_by": "openai-internal"
+ },
+ {
+ "id": "o1-2024-12-17",
+ "object": "model",
+ "created": 1734326976,
+ "owned_by": "system"
+ },
+ {
+ "id": "davinci-002",
+ "object": "model",
+ "created": 1692634301,
+ "owned_by": "system"
+ },
+ {
+ "id": "gpt-3.5-turbo-1106",
+ "object": "model",
+ "created": 1698959748,
+ "owned_by": "system"
+ },
+ {
+ "id": "gpt-4o-search-preview",
+ "object": "model",
+ "created": 1741388720,
+ "owned_by": "system"
+ },
+ {
+ "id": "gpt-3.5-turbo-instruct",
+ "object": "model",
+ "created": 1692901427,
+ "owned_by": "system"
+ },
+ {
+ "id": "gpt-3.5-turbo",
+ "object": "model",
+ "created": 1677610602,
+ "owned_by": "openai"
+ },
+ {
+ "id": "gpt-4o-mini-search-preview-2025-03-11",
+ "object": "model",
+ "created": 1741390858,
+ "owned_by": "system"
+ },
+ {
+ "id": "gpt-4-0125-preview",
+ "object": "model",
+ "created": 1706037612,
+ "owned_by": "system"
+ },
+ {
+ "id": "gpt-4o-2024-11-20",
+ "object": "model",
+ "created": 1739331543,
+ "owned_by": "system"
+ },
+ {
+ "id": "whisper-1",
+ "object": "model",
+ "created": 1677532384,
+ "owned_by": "openai-internal"
+ },
+ {
+ "id": "gpt-4o-2024-05-13",
+ "object": "model",
+ "created": 1715368132,
+ "owned_by": "system"
+ },
+ {
+ "id": "o1-pro",
+ "object": "model",
+ "created": 1742251791,
+ "owned_by": "system"
+ },
+ {
+ "id": "o1-pro-2025-03-19",
+ "object": "model",
+ "created": 1742251504,
+ "owned_by": "system"
+ },
+ {
+ "id": "o1-preview",
+ "object": "model",
+ "created": 1725648897,
+ "owned_by": "system"
+ },
+ {
+ "id": "gpt-4-0613",
+ "object": "model",
+ "created": 1686588896,
+ "owned_by": "openai"
+ },
+ {
+ "id": "gpt-image-1",
+ "object": "model",
+ "created": 1745517030,
+ "owned_by": "system"
+ },
+ {
+ "id": "text-embedding-3-large",
+ "object": "model",
+ "created": 1705953180,
+ "owned_by": "system"
+ },
+ {
+ "id": "gpt-4o-mini-tts",
+ "object": "model",
+ "created": 1742403959,
+ "owned_by": "system"
+ },
+ {
+ "id": "gpt-4o-transcribe",
+ "object": "model",
+ "created": 1742068463,
+ "owned_by": "system"
+ },
+ {
+ "id": "gpt-4.5-preview",
+ "object": "model",
+ "created": 1740623059,
+ "owned_by": "system"
+ },
+ {
+ "id": "gpt-4.5-preview-2025-02-27",
+ "object": "model",
+ "created": 1740623304,
+ "owned_by": "system"
+ },
+ {
+ "id": "gpt-4o-mini-transcribe",
+ "object": "model",
+ "created": 1742068596,
+ "owned_by": "system"
+ },
+ {
+ "id": "gpt-4o-search-preview-2025-03-11",
+ "object": "model",
+ "created": 1741388170,
+ "owned_by": "system"
+ },
+ {
+ "id": "omni-moderation-2024-09-26",
+ "object": "model",
+ "created": 1732734466,
+ "owned_by": "system"
+ },
+ {
+ "id": "o3-mini",
+ "object": "model",
+ "created": 1737146383,
+ "owned_by": "system"
+ },
+ {
+ "id": "o3-mini-2025-01-31",
+ "object": "model",
+ "created": 1738010200,
+ "owned_by": "system"
+ },
+ {
+ "id": "tts-1-hd",
+ "object": "model",
+ "created": 1699046015,
+ "owned_by": "system"
+ },
+ {
+ "id": "gpt-4o",
+ "object": "model",
+ "created": 1715367049,
+ "owned_by": "system"
+ },
+ {
+ "id": "tts-1-hd-1106",
+ "object": "model",
+ "created": 1699053533,
+ "owned_by": "system"
+ },
+ {
+ "id": "gpt-4o-mini",
+ "object": "model",
+ "created": 1721172741,
+ "owned_by": "system"
+ },
+ {
+ "id": "gpt-4o-2024-08-06",
+ "object": "model",
+ "created": 1722814719,
+ "owned_by": "system"
+ },
+ {
+ "id": "gpt-4.1",
+ "object": "model",
+ "created": 1744316542,
+ "owned_by": "system"
+ },
+ {
+ "id": "gpt-4.1-2025-04-14",
+ "object": "model",
+ "created": 1744315746,
+ "owned_by": "system"
+ },
+ {
+ "id": "gpt-4o-mini-2024-07-18",
+ "object": "model",
+ "created": 1721172717,
+ "owned_by": "system"
+ },
+ {
+ "id": "gpt-4o-mini-audio-preview-2024-12-17",
+ "object": "model",
+ "created": 1734115920,
+ "owned_by": "system"
+ },
+ {
+ "id": "gpt-3.5-turbo-0125",
+ "object": "model",
+ "created": 1706048358,
+ "owned_by": "system"
+ },
+ {
+ "id": "gpt-4o-realtime-preview-2024-12-17",
+ "object": "model",
+ "created": 1733945430,
+ "owned_by": "system"
+ },
+ {
+ "id": "gpt-4-turbo",
+ "object": "model",
+ "created": 1712361441,
+ "owned_by": "system"
+ },
+ {
+ "id": "tts-1",
+ "object": "model",
+ "created": 1681940951,
+ "owned_by": "openai-internal"
+ },
+ {
+ "id": "gpt-4-turbo-2024-04-09",
+ "object": "model",
+ "created": 1712601677,
+ "owned_by": "system"
+ },
+ {
+ "id": "tts-1-1106",
+ "object": "model",
+ "created": 1699053241,
+ "owned_by": "system"
+ },
+ {
+ "id": "omni-moderation-latest",
+ "object": "model",
+ "created": 1731689265,
+ "owned_by": "system"
+ },
+ {
+ "id": "o4-mini-2025-04-16",
+ "object": "model",
+ "created": 1744133506,
+ "owned_by": "system"
+ },
+ {
+ "id": "o4-mini",
+ "object": "model",
+ "created": 1744225351,
+ "owned_by": "system"
+ }
+ ]
+ }
+ recorded_at: Wed, 07 May 2025 08:07:48 GMT
+- request:
+ method: post
+ uri: https://api.openai.com/v1/chat/completions
+ body:
+ encoding: UTF-8
+ string: '{"model":"gpt-4o","messages":[{"role":"system","content":"Proofread
+ the provided text, correcting grammatical and spelling errors without altering
+ any HTML markup. Ensure you analyze the text, identify both grammatical and
+ spelling mistakes, and make necessary corrections for clarity and accuracy,
+ including sentence structure, punctuation, verb tense consistency, and correct
+ word usage. Apply all changes directly to the text to enhance readability
+ and flow, ensuring the end result communicates efficiently and professionally.\n\n#
+ Steps\n\n1. **Initial Read-Through**: Carefully read the text once to understand
+ the context and main message.\n2. **Identify Errors**: Examine the text for
+ grammatical and spelling errors including incorrect punctuation, verb tense
+ inconsistencies, improper sentence structures, and incorrect word usage.\n3.
+ **Correction Process**: Make necessary grammatical and spelling corrections,
+ ensuring not to alter any HTML markup present in the text.\n4. **Readability
+ and Flow**: Directly amend the text to improve readability and flow while
+ maintaining the original intent.\n5. **Review**: Double-check the text to
+ ensure no HTML tags are broken and that all corrections maintain coherence
+ and professionalism.\n\n# Output Format\n\n- Provide the fully corrected text
+ with no broken HTML markup.\n\n# Notes\n\n- Retain existing HTML tags; ensure
+ they remain unaltered in the corrected text.\n- Maintain the text in its original
+ language, as the target language should always be used.\n- Prioritize clarity
+ and professionalism in all corrections.\n\nPlease find the text requiring
+ proofreading below:\n"},{"role":"user","content":"I Nicole Braun."}],"temperature":0.1,"response_format":{"type":"text"},"stream":false,"store":false}'
+ headers:
+ Accept-Encoding:
+ - gzip;q=1.0,deflate;q=0.6,identity;q=0.3
+ Accept:
+ - "*/*"
+ User-Agent:
+ - Zammad User Agent
+ Host:
+ - api.openai.com
+ Content-Type:
+ - application/json; charset=utf-8
+ Authorization:
+ - Bearer
+ response:
+ status:
+ code: 200
+ message: OK
+ headers:
+ Date:
+ - Wed, 07 May 2025 08:07:49 GMT
Content-Type:
- application/json
Transfer-Encoding:
@@ -44,73 +553,79 @@ http_interactions:
Openai-Organization:
- zammad-gmbh
Openai-Processing-Ms:
- - '807'
+ - '371'
Openai-Version:
- '2020-10-01'
X-Ratelimit-Limit-Requests:
- - '10000'
+ - '500'
X-Ratelimit-Limit-Tokens:
- - '200000'
+ - '30000'
X-Ratelimit-Remaining-Requests:
- - '9999'
+ - '499'
X-Ratelimit-Remaining-Tokens:
- - '199808'
+ - '29604'
X-Ratelimit-Reset-Requests:
- - 8.64s
+ - 120ms
X-Ratelimit-Reset-Tokens:
- - 57ms
+ - 792ms
X-Request-Id:
- - req_2476493d7f505208bc72baf2a90f6e47
+ - req_f988f9c536dac31c4b7495c466d9e4dc
Strict-Transport-Security:
- max-age=31536000; includeSubDomains; preload
Cf-Cache-Status:
- DYNAMIC
Set-Cookie:
- - __cf_bm=nm34B.0PQB8Jv_cZivP3ENreNByuC7SbEAasUxe0Qpg-1728650445-1.0.1.1-BjIAj0oCZliYo5BctE4pPoNJ1sFSUlvWunAIoRDRvnft4k2U3MJDJsbO.t78nDzUVwxXLg.3xojyYgb9ASrxog;
- path=/; expires=Fri, 11-Oct-24 13:10:45 GMT; domain=.api.openai.com; HttpOnly;
+ - __cf_bm=nBgHaxA2s14TYnQv03Ea5OMRXHMNDEfJ6lI51dysl6w-1746605269-1.0.1.1-EFhEsSjlyblHJOHKHT4mfGbVv_.57dKY5waJJFG.mIkkD5UzE3FT1rf.c8M4tY5FhqgAsMmjFt8NY4n8JBnDxXY3DlDw.GpHioRsZQDn8Oc;
+ path=/; expires=Wed, 07-May-25 08:37:49 GMT; domain=.api.openai.com; HttpOnly;
Secure; SameSite=None
- - _cfuvid=2vlEgAOCQNTMefJFnSbK6dwOiDb9yRwVshejfaG7akg-1728650445857-0.0.1.1-604800000;
+ - _cfuvid=WVVUzfevnxVew8G4eRN_MtaaaUugp_2Ly7S1d89GF8w-1746605269395-0.0.1.1-604800000;
path=/; domain=.api.openai.com; HttpOnly; Secure; SameSite=None
X-Content-Type-Options:
- nosniff
Server:
- cloudflare
Cf-Ray:
- - 8d0eeba09bd8cab9-HAM
+ - 93bf39d25be519af-FRA
Alt-Svc:
- h3=":443"; ma=86400
body:
encoding: ASCII-8BIT
string: |
{
- "id": "chatcmpl-AH9EjYKjmSmYcLzdFSQ7etweyquSy",
+ "id": "chatcmpl-BUU6eNxSzLL1OwrBF4dibrbF5EzEd",
"object": "chat.completion",
- "created": 1728650445,
- "model": "gpt-4o-mini-2024-07-18",
+ "created": 1746605268,
+ "model": "gpt-4o-2024-08-06",
"choices": [
{
"index": 0,
"message": {
"role": "assistant",
- "content": "{\n \"text\": \"I am Nicole Braun.\",\n \"reason\": \"Corrected the sentence to include the verb 'am' for proper grammatical structure.\"\n}",
- "refusal": null
+ "content": "I am Nicole Braun.",
+ "refusal": null,
+ "annotations": []
},
"logprobs": null,
"finish_reason": "stop"
}
],
"usage": {
- "prompt_tokens": 145,
- "completion_tokens": 33,
- "total_tokens": 178,
+ "prompt_tokens": 299,
+ "completion_tokens": 6,
+ "total_tokens": 305,
"prompt_tokens_details": {
- "cached_tokens": 0
+ "cached_tokens": 0,
+ "audio_tokens": 0
},
"completion_tokens_details": {
- "reasoning_tokens": 0
+ "reasoning_tokens": 0,
+ "audio_tokens": 0,
+ "accepted_prediction_tokens": 0,
+ "rejected_prediction_tokens": 0
}
},
- "system_fingerprint": "fp_e2bde53e6e"
+ "service_tier": "default",
+ "system_fingerprint": "fp_90122d973c"
}
- recorded_at: Fri, 11 Oct 2024 12:40:45 GMT
+ recorded_at: Wed, 07 May 2025 08:07:49 GMT
recorded_with: VCR 6.3.1
diff --git a/test/data/vcr_cassettes/lib/ai/service/text_spelling_and_grammar/ai_service_textspellingandgrammar_when_service_is_executed_with_zammadai_as_provider_check_that_grammar_is_correct.yml b/test/data/vcr_cassettes/lib/ai/service/text_spelling_and_grammar/ai_service_textspellingandgrammar_when_service_is_executed_with_zammadai_as_provider_check_that_grammar_is_correct.yml
index dca9efe6d1..b8bb4b63cb 100644
--- a/test/data/vcr_cassettes/lib/ai/service/text_spelling_and_grammar/ai_service_textspellingandgrammar_when_service_is_executed_with_zammadai_as_provider_check_that_grammar_is_correct.yml
+++ b/test/data/vcr_cassettes/lib/ai/service/text_spelling_and_grammar/ai_service_textspellingandgrammar_when_service_is_executed_with_zammadai_as_provider_check_that_grammar_is_correct.yml
@@ -1,20 +1,89 @@
---
http_interactions:
+- request:
+ method: get
+ uri: "/api/v1/me"
+ body:
+ encoding: US-ASCII
+ string: ''
+ headers:
+ Accept-Encoding:
+ - gzip;q=1.0,deflate;q=0.6,identity;q=0.3
+ Accept:
+ - "*/*"
+ User-Agent:
+ - Zammad User Agent
+ Host:
+ - ai.edenhofer.de
+ Authorization:
+ - Bearer
+ response:
+ status:
+ code: 200
+ message: OK
+ headers:
+ Server:
+ - nginx
+ Date:
+ - Wed, 07 May 2025 08:07:49 GMT
+ Content-Type:
+ - application/json; charset=utf-8
+ Transfer-Encoding:
+ - chunked
+ Connection:
+ - keep-alive
+ X-Frame-Options:
+ - SAMEORIGIN
+ X-Xss-Protection:
+ - '0'
+ X-Content-Type-Options:
+ - nosniff
+ X-Permitted-Cross-Domain-Policies:
+ - none
+ Referrer-Policy:
+ - strict-origin-when-cross-origin
+ Vary:
+ - Accept
+ Etag:
+ - W/"fe50ebbaa41ce0a7faa99cc4b8b65660"
+ Cache-Control:
+ - max-age=0, private, must-revalidate
+ X-Request-Id:
+ - 1efba2a8-666b-4947-bfb8-9412ad8e4654
+ X-Runtime:
+ - '0.005978'
+ Strict-Transport-Security:
+ - max-age=63072000; includeSubDomains
+ body:
+ encoding: ASCII-8BIT
+ string: '{"current_user":{"id":3,"email":"me@edenhofer.de","firstname":"Martin","lastname":"Edenhofer","active":true,"created_by_id":1,"created_at":"2024-07-15T23:15:06.140Z","updated_at":"2025-05-03T20:58:06.103Z","organization_id":1,"is_platform_admin":true,"is_organization_admin":false,"login_failed_count":0,"login_at":"2025-05-03T20:58:06.095Z"}}'
+ recorded_at: Wed, 07 May 2025 08:07:49 GMT
- request:
method: post
- uri: https://ai.edenhofer.de/api/v1/features/text_spelling_and_grammar
+ uri: "/api/v1/features/text_spelling_and_grammar"
body:
encoding: UTF-8
- string: '{"system_prompt":"You are a helpful support agent. Your task is to
- fix the spelling, grammar, and punctuation of the following response while
- keeping it in the same language as the user''s input. Detect the language
- from the input and ensure corrections are made in the same language. Ensure
- that the reply remains polite and professional.\n\nAdditionally, if the response
- contains any HTML content, do not break or remove the HTML tags. Make corrections
- only to the text within the tags without altering the HTML structure.\n\nAdd
- also the reason what you have done to response.\n\nPlease respond using the
- defined JSON syntax provided below:\n{\n“text”: “string”,\n“reason”, “string”\n}\n","prompt":"I
- Nicole Braun.","options":{}}'
+ string: '{"system_prompt":"Proofread the provided text, correcting grammatical
+ and spelling errors without altering any HTML markup. Ensure you analyze the
+ text, identify both grammatical and spelling mistakes, and make necessary
+ corrections for clarity and accuracy, including sentence structure, punctuation,
+ verb tense consistency, and correct word usage. Apply all changes directly
+ to the text to enhance readability and flow, ensuring the end result communicates
+ efficiently and professionally.\n\n# Steps\n\n1. **Initial Read-Through**:
+ Carefully read the text once to understand the context and main message.\n2.
+ **Identify Errors**: Examine the text for grammatical and spelling errors
+ including incorrect punctuation, verb tense inconsistencies, improper sentence
+ structures, and incorrect word usage.\n3. **Correction Process**: Make necessary
+ grammatical and spelling corrections, ensuring not to alter any HTML markup
+ present in the text.\n4. **Readability and Flow**: Directly amend the text
+ to improve readability and flow while maintaining the original intent.\n5.
+ **Review**: Double-check the text to ensure no HTML tags are broken and that
+ all corrections maintain coherence and professionalism.\n\n# Output Format\n\n-
+ Provide the fully corrected text with no broken HTML markup.\n\n# Notes\n\n-
+ Retain existing HTML tags; ensure they remain unaltered in the corrected text.\n-
+ Maintain the text in its original language, as the target language should
+ always be used.\n- Prioritize clarity and professionalism in all corrections.\n\nPlease
+ find the text requiring proofreading below:\n","prompt":"I Nicole Braun."}'
headers:
Accept-Encoding:
- gzip;q=1.0,deflate;q=0.6,identity;q=0.3
@@ -36,7 +105,7 @@ http_interactions:
Server:
- nginx
Date:
- - Fri, 11 Oct 2024 12:40:47 GMT
+ - Wed, 07 May 2025 08:07:49 GMT
Content-Type:
- application/json; charset=utf-8
Transfer-Encoding:
@@ -56,21 +125,18 @@ http_interactions:
Vary:
- Accept
Etag:
- - W/"4317d05a619dd85cb5a20f1d2d50090f"
+ - W/"43b2efd4d096cb3c3dccbb311d140077"
Cache-Control:
- max-age=0, private, must-revalidate
X-Request-Id:
- - 4bbe9cef-2e08-4c6d-bec6-4a28cff47e34
+ - 99333fc3-ec81-4346-a252-a0c8615ebeb1
X-Runtime:
- - '0.857255'
+ - '0.320539'
Strict-Transport-Security:
- max-age=63072000; includeSubDomains
body:
encoding: ASCII-8BIT
- string: '[{"model":"llama3.1","created_at":"2024-10-11T12:40:46.927156869Z","response":"{\n \"text\":
- \"Hello, I am Nicole Braun.\",\n \"reason\": \"The original text was a single
- sentence with no punctuation, so I added a period at the end to make it a
- complete sentence and also capitalized ''I'' as it''s the first word of the
- sentence.\"\n}","done":true,"done_reason":"stop","context":[128006,882,128007,271,2675,527,264,11190,1862,8479,13,4718,3465,374,311,5155,279,43529,11,32528,11,323,62603,315,279,2768,2077,1418,10494,433,304,279,1890,4221,439,279,1217,596,1988,13,34387,279,4221,505,279,1988,323,6106,51479,527,1903,304,279,1890,4221,13,30379,430,279,10052,8625,48887,323,6721,382,50674,11,422,279,2077,5727,904,9492,2262,11,656,539,1464,477,4148,279,9492,9681,13,7557,51479,1193,311,279,1495,2949,279,9681,2085,60923,279,9492,6070,382,2261,1101,279,2944,1148,499,617,2884,311,2077,382,5618,6013,1701,279,4613,4823,20047,3984,3770,512,517,2118,1342,57633,1054,928,863,345,2118,20489,9520,1054,928,89874,3818,40,45130,70703,13,128009,128006,78191,128007,271,517,220,330,1342,794,330,9906,11,358,1097,45130,70703,10560,220,330,20489,794,330,791,4113,1495,574,264,3254,11914,449,912,62603,11,779,358,3779,264,4261,520,279,842,311,1304,433,264,4686,11914,323,1101,98421,364,40,6,439,433,596,279,1176,3492,315,279,11914,10246,92],"total_duration":608398872,"load_duration":18389827,"prompt_eval_count":145,"prompt_eval_duration":39144000,"eval_count":61,"eval_duration":508861000,"uuid":"4bbe9cef-2e08-4c6d-bec6-4a28cff47e34"}]'
- recorded_at: Fri, 11 Oct 2024 12:40:47 GMT
+ string: '[{"model":"gemma3:27b","created_at":"2025-05-07T08:07:49.959841933Z","response":"I
+ am Nicole Braun.\n","done":true,"done_reason":"stop","context":[105,2364,107,14476,1399,506,3847,1816,236764,74240,77536,532,49248,9825,2180,66035,1027,16167,71911,236761,41152,611,18840,506,1816,236764,8701,1800,77536,532,49248,23153,236764,532,1386,4127,38565,573,29972,532,10634,236764,2440,13315,3904,236764,88078,236764,14177,54817,25725,236764,532,4338,3658,14120,236761,30839,784,3731,5467,531,506,1816,531,14051,122499,532,2727,236764,17096,506,1345,1354,83043,23057,532,46766,236761,108,236865,50228,108,236770,236761,5213,22995,8847,236772,24452,66515,151531,1676,506,1816,3622,531,3050,506,4403,532,1689,3618,236761,107,236778,236761,5213,137938,77905,66515,160153,506,1816,573,77536,532,49248,9825,2440,21648,88078,236764,14177,54817,114461,236764,35503,13315,9108,236764,532,21648,3658,14120,236761,107,236800,236761,5213,135778,12364,66515,9996,4127,77536,532,49248,38565,236764,17096,711,531,9702,1027,16167,71911,1861,528,506,1816,236761,107,236812,236761,5213,6190,2109,532,19179,66515,181141,22802,506,1816,531,4006,122499,532,2727,1651,16977,506,3303,9703,236761,107,236810,236761,5213,30852,66515,16193,236772,4256,506,1816,531,5330,951,16167,16616,659,11207,532,600,784,38565,4883,64106,532,66672,236761,108,236865,16887,36353,108,236772,39525,506,6340,32107,1816,607,951,11207,16167,71911,236761,108,236865,24118,108,236772,5578,662,6330,16167,16616,236793,5330,901,4595,145394,528,506,32107,1816,236761,107,236772,75133,506,1816,528,1061,3303,5192,236764,618,506,3328,5192,1374,2462,577,1456,236761,107,236772,22282,77372,29972,532,66672,528,784,38565,236761,108,9366,1586,506,1816,20226,7724,37974,3426,236787,109,236777,46562,64876,236761,106,107,105,4368,107,236777,1006,46562,64876,236761,107],"total_duration":256835711,"load_duration":63814426,"prompt_eval_count":301,"prompt_eval_duration":90798721,"eval_count":7,"eval_duration":101694728,"uuid":"99333fc3-ec81-4346-a252-a0c8615ebeb1"}]'
+ recorded_at: Wed, 07 May 2025 08:07:49 GMT
recorded_with: VCR 6.3.1