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 @@ + + + + + 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