mirror of
https://github.com/zammad/zammad
synced 2026-05-24 09:48:36 +00:00
Feature: Zammad Smart Assist - Agent article reply with assistance AI tools
Co-authored-by: Benjamin Scharf <bs@zammad.com> Co-authored-by: Dusan Vuckovic <dv@zammad.com> Co-authored-by: Dominik Klein <dk@zammad.com>
This commit is contained in:
parent
db69446d6c
commit
a2136ce304
71 changed files with 2648 additions and 318 deletions
|
|
@ -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) ->
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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 = $('<div />')
|
||||
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
|
||||
|
|
|
|||
96
app/assets/javascripts/app/lib/mixins/text_tools.coffee
Normal file
96
app/assets/javascripts/app/lib/mixins/text_tools.coffee
Normal file
|
|
@ -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
|
||||
20
app/assets/javascripts/app/views/generic/text_tools.jst.eco
Normal file
20
app/assets/javascripts/app/views/generic/text_tools.jst.eco
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
<% if !@disabled: %>
|
||||
<div class="text-tools <% if @no_attachment: %>text-tools--standalone<% end %>">
|
||||
<%- @Icon('smart-assist-elaborate') %>
|
||||
<div class="buttonDropdown dropdown dropdown--actions dropup">
|
||||
<a id="textToolsAction" class="text-tools-action" href="#" data-toggle="dropdown">
|
||||
<%- @T('Smart Editor') %>
|
||||
</a>
|
||||
<ul class="dropdown-menu" role="menu" aria-labelledby="textToolsAction">
|
||||
<li class="js-action" role="menuitem" data-type="improve_writing">
|
||||
<%- @T('Improve writing') %>
|
||||
<li class="js-action" role="menuitem" data-type="spelling_and_grammar">
|
||||
<%- @T('Fix spelling and grammar') %>
|
||||
<li class="js-action" role="menuitem" data-type="expand">
|
||||
<%- @T('Expand') %>
|
||||
<li class="js-action" role="menuitem" data-type="simplify">
|
||||
<%- @T('Simplify') %>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
<div class="text-tools-loading js-loading">
|
||||
<%- @Icon('smart-assist-elaborate') %><%- @T('Smart Assist is generating text…') %>
|
||||
<a class="js-cancel" href="#"><%- @T('Cancel') %></a>
|
||||
</div>
|
||||
|
|
@ -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; }
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
17
app/controllers/ai_assistance_controller.rb
Normal file
17
app/controllers/ai_assistance_controller.rb
Normal file
|
|
@ -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
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
<svg width="20" height="20" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M12.5931 7.59314L11.5931 11.5931L10.5931 7.59314L6.59314 6.59314L10.5931 5.59314L11.5931 1.59314L12.5931 5.59314L16.5931 6.59314L12.5931 7.59314Z" />
|
||||
<path d="M14.7056 3.98408L17.8973 3.58512C18.0117 3.57082 18.0151 3.40621 17.9013 3.38725L15.6484 3.01177C15.6063 3.00475 15.5733 2.97172 15.5662 2.92957L15.1908 0.676701C15.1718 0.562964 15.0072 0.566322 14.9929 0.680738L14.5939 3.87245C14.5858 3.93716 14.6409 3.99217 14.7056 3.98408Z" />
|
||||
<path d="M0.59314 11.0931C0.59314 10.817 0.816997 10.5931 1.09314 10.5931H8.09314C8.36928 10.5931 8.59314 10.817 8.59314 11.0931C8.59314 11.3693 8.36928 11.5931 8.09314 11.5931H1.09314C0.816997 11.5931 0.59314 11.3693 0.59314 11.0931Z" />
|
||||
<path d="M0.59314 14.0931C0.59314 13.817 0.816997 13.5931 1.09314 13.5931H16.0931C16.3693 13.5931 16.5931 13.817 16.5931 14.0931C16.5931 14.3693 16.3693 14.5931 16.0931 14.5931H1.09314C0.816997 14.5931 0.59314 14.3693 0.59314 14.0931Z" />
|
||||
<path d="M0.59314 17.0931C0.59314 16.817 0.816997 16.5931 1.09314 16.5931H7.09314C7.36928 16.5931 7.59314 16.817 7.59314 17.0931C7.59314 17.3693 7.36928 17.5931 7.09314 17.5931H1.09314C0.816997 17.5931 0.59314 17.3693 0.59314 17.0931Z" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
|
|
@ -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',
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,7 @@
|
|||
<svg width="20" height="20" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M12.5931 7.59311L11.5931 11.5931L10.5931 7.59311L6.59314 6.59311L10.5931 5.59311L11.5931 1.59311L12.5931 5.59311L16.5931 6.59311L12.5931 7.59311Z" />
|
||||
<path d="M14.7056 3.98405L17.8973 3.58509C18.0117 3.57079 18.0151 3.40618 17.9013 3.38722L15.6484 3.01174C15.6063 3.00472 15.5733 2.97169 15.5662 2.92954L15.1908 0.676671C15.1718 0.562934 15.0072 0.566291 14.9929 0.680707L14.5939 3.87242C14.5858 3.93713 14.6409 3.99214 14.7056 3.98405Z" />
|
||||
<path d="M0.59314 11.0931C0.59314 10.817 0.816997 10.5931 1.09314 10.5931H8.09314C8.36928 10.5931 8.59314 10.817 8.59314 11.0931C8.59314 11.3693 8.36928 11.5931 8.09314 11.5931H1.09314C0.816997 11.5931 0.59314 11.3693 0.59314 11.0931Z" />
|
||||
<path d="M0.59314 14.0931C0.59314 13.817 0.816997 13.5931 1.09314 13.5931H16.0931C16.3693 13.5931 16.5931 13.817 16.5931 14.0931C16.5931 14.3693 16.3693 14.5931 16.0931 14.5931H1.09314C0.816997 14.5931 0.59314 14.3693 0.59314 14.0931Z" />
|
||||
<path d="M0.59314 17.0931C0.59314 16.817 0.816997 16.5931 1.09314 16.5931H7.09314C7.36928 16.5931 7.59314 16.817 7.59314 17.0931C7.59314 17.3693 7.36928 17.5931 7.09314 17.5931H1.09314C0.816997 17.5931 0.59314 17.3693 0.59314 17.0931Z" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
|
|
@ -0,0 +1,7 @@
|
|||
<svg width="20" height="20" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M11.5709 11.7164L10.1941 17.2239C10.1436 17.4259 9.8565 17.4259 9.806 17.2239L8.42913 11.7164C8.41122 11.6448 8.35527 11.5888 8.28361 11.5709L2.77614 10.1941C2.57415 10.1436 2.57415 9.8565 2.77614 9.806L8.28361 8.42913C8.35527 8.41122 8.41122 8.35527 8.42913 8.28361L9.806 2.77614C9.8565 2.57415 10.1436 2.57415 10.1941 2.77614L11.5709 8.28361C11.5888 8.35527 11.6448 8.41122 11.7164 8.42913L17.2239 9.806C17.4259 9.8565 17.4259 10.1436 17.2239 10.1941L11.7164 11.5709C11.6448 11.5888 11.5888 11.6448 11.5709 11.7164Z" />
|
||||
<path d="M4.87245 15.016L1.68074 15.4149C1.56632 15.4292 1.56296 15.5939 1.6767 15.6128L3.92957 15.9883C3.97172 15.9953 4.00475 16.0283 4.01177 16.0705L4.38725 18.3234C4.40621 18.4371 4.57082 18.4337 4.58512 18.3193L4.98408 15.1276C4.99217 15.0629 4.93716 15.0079 4.87245 15.016Z" />
|
||||
<path d="M15.1276 15.016L18.3193 15.4149C18.4337 15.4292 18.4371 15.5939 18.3234 15.6128L16.0705 15.9883C16.0283 15.9953 15.9953 16.0283 15.9883 16.0705L15.6128 18.3234C15.5939 18.4371 15.4292 18.4337 15.4149 18.3193L15.016 15.1276C15.0079 15.0629 15.0629 15.0079 15.1276 15.016Z" />
|
||||
<path d="M4.87245 4.98408L1.68074 4.58512C1.56632 4.57082 1.56296 4.40621 1.6767 4.38725L3.92957 4.01177C3.97172 4.00475 4.00475 3.97172 4.01177 3.92957L4.38725 1.6767C4.40621 1.56296 4.57082 1.56632 4.58512 1.68074L4.98408 4.87245C4.99217 4.93716 4.93716 4.99217 4.87245 4.98408Z" />
|
||||
<path d="M15.1276 4.98408L18.3193 4.58512C18.4337 4.57082 18.4371 4.40621 18.3234 4.38725L16.0705 4.01177C16.0283 4.00475 15.9953 3.97172 15.9883 3.92957L15.6128 1.6767C15.5939 1.56296 15.4292 1.56632 15.4149 1.68074L15.016 4.87245C15.0079 4.93716 15.0629 4.99217 15.1276 4.98408Z" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.7 KiB |
|
|
@ -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',
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -197,7 +197,10 @@ const leftgradientvalue = computed(() => classes.actionBar.leftGradient.left)
|
|||
/>
|
||||
</button>
|
||||
<div v-if="action.showDivider">
|
||||
<hr class="h-full w-px border-0 bg-neutral-100 dark:bg-gray-900" />
|
||||
<hr
|
||||
:class="action.dividerClass"
|
||||
class="h-full w-px border-0 bg-neutral-100 dark:bg-gray-900"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,54 @@
|
|||
<!-- Copyright (C) 2012-2025 Zammad Foundation, https://zammad-foundation.org/ -->
|
||||
|
||||
<script setup lang="ts">
|
||||
import CommonTranslateRenderer from '#shared/components/CommonTranslateRenderer/CommonTranslateRenderer.vue'
|
||||
import { getAiAssistantTextToolsLoadingBannerClasses } from '#shared/components/Form/fields/FieldEditor/AiAssistantTextTools/initializeAiAssistantTextToolsLoadingBanner.ts'
|
||||
import { useAppName } from '#shared/composables/useAppName.ts'
|
||||
|
||||
import type { Editor } from '@tiptap/core'
|
||||
|
||||
defineProps<{
|
||||
editor?: Editor
|
||||
}>()
|
||||
|
||||
const { icon, label, button } = getAiAssistantTextToolsLoadingBannerClasses()
|
||||
|
||||
const appName = useAppName()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="ai-stripe relative flex items-center gap-1 px-4 py-3 before:absolute before:top-0 before:left-0"
|
||||
>
|
||||
<CommonIcon
|
||||
class="shrink-0"
|
||||
:class="icon"
|
||||
size="tiny"
|
||||
name="smart-assist"
|
||||
/>
|
||||
|
||||
<CommonTranslateRenderer
|
||||
v-if="appName === 'desktop'"
|
||||
class="truncate text-sm"
|
||||
:source="__('%s is generating text')"
|
||||
:placeholders="[
|
||||
{
|
||||
type: 'label',
|
||||
props: {
|
||||
class: label,
|
||||
},
|
||||
content: $t('Zammad Smart Assist'),
|
||||
},
|
||||
]"
|
||||
/>
|
||||
<CommonLabel v-else>{{ $t('Generating text…') }}</CommonLabel>
|
||||
|
||||
<button
|
||||
class="text-sm ltr:ml-auto rtl:mr-auto"
|
||||
:class="button"
|
||||
@click="editor?.emit('cancel-ai-assistant-text-tools-updates')"
|
||||
>
|
||||
{{ $t('Cancel') }}
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,229 @@
|
|||
<!-- Copyright (C) 2012-2025 Zammad Foundation, https://zammad-foundation.org/ -->
|
||||
|
||||
<script setup lang="ts">
|
||||
import { DOMSerializer } from 'prosemirror-model'
|
||||
import { computed, watch } from 'vue'
|
||||
|
||||
import {
|
||||
useNotifications,
|
||||
NotificationTypes,
|
||||
} from '#shared/components/CommonNotifications/index.ts'
|
||||
import { getAiAssistantTextToolsClasses } from '#shared/components/Form/fields/FieldEditor/AiAssistantTextTools/initializeAiAssistantTextTools.ts'
|
||||
import type { FieldEditorProps } from '#shared/components/Form/fields/FieldEditor/types.ts'
|
||||
import type { FormFieldContext } from '#shared/components/Form/types/field.ts'
|
||||
import { useAiAssistanceTextToolsMutation } from '#shared/graphql/mutations/aiAssistanceTextTools.api.ts'
|
||||
import { EnumAiTextToolService } from '#shared/graphql/types.ts'
|
||||
import { MutationHandler } from '#shared/server/apollo/handler/index.ts'
|
||||
|
||||
import type { Editor } from '@tiptap/vue-3'
|
||||
|
||||
const props = defineProps<{
|
||||
editor?: Editor
|
||||
formContext?: FormFieldContext<FieldEditorProps>
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
close: []
|
||||
action: []
|
||||
'hide-action-bar': [boolean]
|
||||
'show-ai-text-loader': [boolean]
|
||||
}>()
|
||||
|
||||
const smartEditorClasses = getAiAssistantTextToolsClasses()
|
||||
|
||||
const { notify } = useNotifications()
|
||||
|
||||
const hasSelection = computed(
|
||||
() =>
|
||||
props.editor?.state.selection.anchor !== props.editor?.state.selection.head,
|
||||
)
|
||||
|
||||
const useAbortableMutation = () => {
|
||||
const abortController = new AbortController()
|
||||
|
||||
const textToolsMutation = new MutationHandler(
|
||||
useAiAssistanceTextToolsMutation({
|
||||
context: { fetchOptions: { signal: abortController.signal } },
|
||||
}),
|
||||
)
|
||||
return {
|
||||
textToolsMutation,
|
||||
isLoading: textToolsMutation.loading(),
|
||||
abortController,
|
||||
abort: () => abortController.abort(),
|
||||
}
|
||||
}
|
||||
|
||||
let aiAssistanceTextToolsController = useAbortableMutation()
|
||||
|
||||
watch(
|
||||
() => props.formContext?.value,
|
||||
() => {
|
||||
if (aiAssistanceTextToolsController.isLoading.value) {
|
||||
notify({
|
||||
id: 'ai-assistant-text-tools-aborted',
|
||||
type: NotificationTypes.Info,
|
||||
message: __(
|
||||
'The text was modified. Your request has been aborted to prevent overwriting.',
|
||||
),
|
||||
})
|
||||
aiAssistanceTextToolsController.abort()
|
||||
aiAssistanceTextToolsController = useAbortableMutation()
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
props.editor?.on('cancel-ai-assistant-text-tools-updates', () => {
|
||||
aiAssistanceTextToolsController.abort()
|
||||
aiAssistanceTextToolsController = useAbortableMutation()
|
||||
})
|
||||
|
||||
const sendTextToolsMutation = async (
|
||||
textToolService: EnumAiTextToolService,
|
||||
input: string,
|
||||
) => {
|
||||
const response = await aiAssistanceTextToolsController.textToolsMutation.send(
|
||||
{
|
||||
input,
|
||||
serviceType: textToolService,
|
||||
},
|
||||
)
|
||||
return response?.aiAssistanceTextTools?.output
|
||||
}
|
||||
|
||||
// :TODO - Custom command maybe?
|
||||
const getSelection = () => props.editor!.state.selection
|
||||
|
||||
// :TODO - Custom command maybe?
|
||||
const getHTMLFromSelection = (selection: Editor['state']['selection']) => {
|
||||
const slice = selection.content()
|
||||
const serializer = DOMSerializer.fromSchema(props.editor!.schema)
|
||||
const fragment = serializer.serializeFragment(slice.content)
|
||||
const div = document.createElement('div')
|
||||
div.appendChild(fragment)
|
||||
|
||||
return div.innerHTML
|
||||
}
|
||||
|
||||
const updateSelectedContent = (content: string) => {
|
||||
props.editor!.commands.deleteSelection()
|
||||
|
||||
// Remove visual newlines from the model which should not play any role.
|
||||
props.editor!.commands.insertContent(content.replace(/\s*\n\s*/g, ''))
|
||||
}
|
||||
|
||||
const hideActionBarAndShowAiTextLoader = () => {
|
||||
emit('close')
|
||||
props.editor!.setEditable(false)
|
||||
emit('hide-action-bar', true)
|
||||
emit('show-ai-text-loader', true)
|
||||
}
|
||||
|
||||
const showActionBarAndHideAiTextLoader = () => {
|
||||
emit('hide-action-bar', false)
|
||||
emit('show-ai-text-loader', false)
|
||||
props.editor!.setEditable(true)
|
||||
}
|
||||
|
||||
const modifySelectedText = async (textToolService: EnumAiTextToolService) => {
|
||||
hideActionBarAndShowAiTextLoader()
|
||||
|
||||
const lastSelection = getSelection()
|
||||
|
||||
const input = getHTMLFromSelection(lastSelection)
|
||||
|
||||
return sendTextToolsMutation(textToolService, input)
|
||||
.then((output) => {
|
||||
if (!output) return
|
||||
|
||||
// Make sure the right selection is always set
|
||||
props.editor?.chain().focus().setTextSelection(lastSelection).run()
|
||||
|
||||
updateSelectedContent(output)
|
||||
})
|
||||
.catch(() => {
|
||||
// Handle abort errors gracefully:
|
||||
// Currently, aborting a request triggers both a warning and an error toast,
|
||||
// as both are instances of ApolloError. Disabling toast messages entirely
|
||||
// would suppress all mutation-related errors, not just abort errors.
|
||||
// TODO: Investigate a way to suppress only abort-related error messages
|
||||
// while preserving other mutation error notifications.
|
||||
props.editor?.chain().focus().setTextSelection(lastSelection).run()
|
||||
})
|
||||
.finally(showActionBarAndHideAiTextLoader)
|
||||
}
|
||||
|
||||
const actions = computed(() => [
|
||||
{
|
||||
key: 'improve-writing',
|
||||
label: __('Improve writing'),
|
||||
disabled: !hasSelection.value,
|
||||
onClick: () => modifySelectedText(EnumAiTextToolService.ImproveWriting),
|
||||
},
|
||||
{
|
||||
key: 'fix-spelling-grammar',
|
||||
label: __('Fix spelling and grammar'),
|
||||
disabled: !hasSelection.value,
|
||||
onClick: () => modifySelectedText(EnumAiTextToolService.SpellingAndGrammar),
|
||||
},
|
||||
{
|
||||
key: 'expand',
|
||||
label: __('Expand'),
|
||||
disabled: !hasSelection.value,
|
||||
onClick: () => modifySelectedText(EnumAiTextToolService.Expand),
|
||||
},
|
||||
{
|
||||
key: 'simplify',
|
||||
label: __('Simplify'),
|
||||
disabled: !hasSelection.value,
|
||||
onClick: () => modifySelectedText(EnumAiTextToolService.Simplify),
|
||||
},
|
||||
])
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="smartEditorClasses.popover.base">
|
||||
<ul ref="list">
|
||||
<li v-for="action in actions" :key="action.key">
|
||||
<button
|
||||
:disabled="action.disabled"
|
||||
:class="smartEditorClasses.popover.button"
|
||||
class="disabled:pointer-events-none disabled:opacity-60"
|
||||
@click="action.onClick"
|
||||
>
|
||||
{{ $t(action.label) }}
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
[data-theme='light'] [contenteditable='false'][name='body'] {
|
||||
color: #a0a3a6;
|
||||
|
||||
* {
|
||||
color: currentColor;
|
||||
}
|
||||
}
|
||||
|
||||
[contenteditable='false'][name='body'],
|
||||
[data-theme='dark'] [contenteditable='false'][name='body'] {
|
||||
color: #999;
|
||||
|
||||
* {
|
||||
color: currentColor;
|
||||
}
|
||||
}
|
||||
|
||||
[data-theme='light'] [contenteditable='false'][name='body'] ::selection {
|
||||
color: #585856;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
[contenteditable='false'][name='body'] ::selection,
|
||||
[data-theme='dark'] [contenteditable='false'][name='body'] ::selection {
|
||||
color: #d1d1d1;
|
||||
background: transparent;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -1,16 +1,23 @@
|
|||
<!-- Copyright (C) 2012-2025 Zammad Foundation, https://zammad-foundation.org/ -->
|
||||
|
||||
<script setup lang="ts">
|
||||
import { nextTick, shallowRef, toRef } from 'vue'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { nextTick, shallowRef, toRef, ref, defineAsyncComponent } from 'vue'
|
||||
|
||||
import CommonPopover from '#shared/components/CommonPopover/CommonPopover.vue'
|
||||
import { usePopover } from '#shared/components/CommonPopover/usePopover.ts'
|
||||
import ActionBar from '#shared/components/Form/fields/FieldEditor/ActionBar.vue'
|
||||
import { getFieldEditorProps } from '#shared/components/Form/initializeFieldEditor.ts'
|
||||
import type { FormFieldContext } from '#shared/components/Form/types/field.ts'
|
||||
import { useApplicationStore } from '#shared/stores/application.ts'
|
||||
|
||||
import useEditorActions, { type EditorButton } from './useEditorActions.ts'
|
||||
|
||||
import type { EditorContentType, EditorCustomPlugins } from './types.ts'
|
||||
import type {
|
||||
EditorContentType,
|
||||
EditorCustomPlugins,
|
||||
FieldEditorProps,
|
||||
} from './types.ts'
|
||||
import type { Selection } from '@tiptap/pm/state'
|
||||
import type { Editor } from '@tiptap/vue-3'
|
||||
import type { Except } from 'type-fest'
|
||||
|
|
@ -21,16 +28,25 @@ const props = defineProps<{
|
|||
contentType: EditorContentType
|
||||
visible: boolean
|
||||
disabledPlugins: EditorCustomPlugins[]
|
||||
formId: string
|
||||
formContext?: FormFieldContext<FieldEditorProps>
|
||||
}>()
|
||||
|
||||
defineEmits<{
|
||||
hide: []
|
||||
hide: [boolean?]
|
||||
blur: []
|
||||
}>()
|
||||
|
||||
const AiAssistantTextToolsLoadingBanner = defineAsyncComponent(
|
||||
() =>
|
||||
import(
|
||||
'#shared/components/Form/fields/FieldEditor/AiAssistantTextTools/AiAssistantLoadingBanner.vue'
|
||||
),
|
||||
)
|
||||
|
||||
const editor = toRef(props, 'editor')
|
||||
|
||||
const hideActionBarLocally = ref(false)
|
||||
|
||||
const { actions, isActive } = useEditorActions(
|
||||
editor,
|
||||
props.contentType,
|
||||
|
|
@ -57,6 +73,7 @@ const handleButtonClick = (action: EditorButton, event: MouseEvent) => {
|
|||
|
||||
subMenuPopoverContent.value = action.subMenu
|
||||
popoverTarget.value = event.currentTarget as HTMLDivElement
|
||||
popoverTarget.value.id = action.id
|
||||
|
||||
nextTick(() => {
|
||||
open()
|
||||
|
|
@ -73,43 +90,68 @@ const handleSubMenuClick = () => {
|
|||
currentSelection = undefined
|
||||
}
|
||||
}
|
||||
|
||||
// :TODO this code should not live here...
|
||||
const showAiAssistantTextToolsLoadingBanner = ref(false)
|
||||
|
||||
const { config } = storeToRefs(useApplicationStore())
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ActionBar
|
||||
v-show="visible || editorProps.actionBar.visible"
|
||||
:editor="editor"
|
||||
:visible="visible"
|
||||
:is-active="isActive"
|
||||
:actions="actions"
|
||||
@click-action="handleButtonClick"
|
||||
@blur="$emit('blur')"
|
||||
@hide="$emit('hide')"
|
||||
/>
|
||||
|
||||
<CommonPopover
|
||||
ref="popover"
|
||||
:owner="popoverTarget"
|
||||
orientation="autoVertical"
|
||||
placement="arrowStart"
|
||||
no-auto-focus
|
||||
>
|
||||
<template v-if="Array.isArray(subMenuPopoverContent)">
|
||||
<ActionBar
|
||||
data-test-id="sub-menu-action-bar"
|
||||
:actions="subMenuPopoverContent"
|
||||
:editor="editor"
|
||||
:is-active="isActive"
|
||||
no-gradient
|
||||
@click-action="handleButtonClick"
|
||||
/>
|
||||
</template>
|
||||
<component
|
||||
:is="subMenuPopoverContent"
|
||||
v-else
|
||||
<div>
|
||||
<ActionBar
|
||||
v-show="
|
||||
!hideActionBarLocally && (visible || editorProps.actionBar.visible)
|
||||
"
|
||||
:editor="editor"
|
||||
:content-type="contentType"
|
||||
@action="handleSubMenuClick"
|
||||
:visible="visible"
|
||||
:is-active="isActive"
|
||||
:actions="actions"
|
||||
@click-action="handleButtonClick"
|
||||
@blur="$emit('blur')"
|
||||
@hide="$emit('hide')"
|
||||
/>
|
||||
</CommonPopover>
|
||||
|
||||
<AiAssistantTextToolsLoadingBanner
|
||||
v-if="
|
||||
showAiAssistantTextToolsLoadingBanner && config.ai_assistance_text_tools
|
||||
"
|
||||
:editor="editor"
|
||||
/>
|
||||
|
||||
<!-- :TODO rethink the persistent -->
|
||||
<CommonPopover
|
||||
ref="popover"
|
||||
:owner="popoverTarget"
|
||||
persistent
|
||||
orientation="autoVertical"
|
||||
placement="arrowStart"
|
||||
no-auto-focus
|
||||
>
|
||||
<template v-if="Array.isArray(subMenuPopoverContent)">
|
||||
<ActionBar
|
||||
:id="popoverTarget?.id"
|
||||
data-test-id="sub-menu-action-bar"
|
||||
:actions="subMenuPopoverContent"
|
||||
:editor="editor"
|
||||
:is-active="isActive"
|
||||
no-gradient
|
||||
@click-action="handleButtonClick"
|
||||
/>
|
||||
</template>
|
||||
<component
|
||||
:is="subMenuPopoverContent"
|
||||
v-else
|
||||
:id="popoverTarget?.id"
|
||||
ref="sub-menu-popover-content"
|
||||
:editor="editor"
|
||||
:content-type="contentType"
|
||||
:form-context="formContext"
|
||||
@action="handleSubMenuClick"
|
||||
@close="close"
|
||||
@hide-action-bar="hideActionBarLocally = $event"
|
||||
@show-ai-text-loader="showAiAssistantTextToolsLoadingBanner = $event"
|
||||
/>
|
||||
</CommonPopover>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -106,3 +106,9 @@ export interface FieldEditorProps {
|
|||
}
|
||||
|
||||
export type EditorCustomPlugins = keyof ConfidentTake<FieldEditorProps, 'meta'>
|
||||
|
||||
declare module '@tiptap/vue-3' {
|
||||
interface EditorEvents {
|
||||
'cancel-ai-assistant-text-tools-updates': void
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<EditorButton, 'subMenu'>[]
|
||||
}
|
||||
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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> = () => TParam;
|
||||
|
||||
export const AiAssistanceTextToolsDocument = gql`
|
||||
mutation aiAssistanceTextTools($input: String!, $serviceType: EnumAITextToolService!) {
|
||||
aiAssistanceTextTools(input: $input, serviceType: $serviceType) {
|
||||
output
|
||||
}
|
||||
}
|
||||
`;
|
||||
export function useAiAssistanceTextToolsMutation(options: VueApolloComposable.UseMutationOptions<Types.AiAssistanceTextToolsMutation, Types.AiAssistanceTextToolsMutationVariables> | ReactiveFunction<VueApolloComposable.UseMutationOptions<Types.AiAssistanceTextToolsMutation, Types.AiAssistanceTextToolsMutationVariables>> = {}) {
|
||||
return VueApolloComposable.useMutation<Types.AiAssistanceTextToolsMutation, Types.AiAssistanceTextToolsMutationVariables>(AiAssistanceTextToolsDocument, options);
|
||||
}
|
||||
export type AiAssistanceTextToolsMutationCompositionFunctionResult = VueApolloComposable.UseMutationReturn<Types.AiAssistanceTextToolsMutation, Types.AiAssistanceTextToolsMutationVariables>;
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
mutation aiAssistanceTextTools($input: String!, $serviceType: EnumAITextToolService!) {
|
||||
aiAssistanceTextTools(input: $input, serviceType: $serviceType) {
|
||||
output
|
||||
}
|
||||
}
|
||||
|
|
@ -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<Types.AiAssistanceTextToolsMutation, Types.AiAssistanceTextToolsMutationVariables>) {
|
||||
return Mocks.mockGraphQLResult(Operations.AiAssistanceTextToolsDocument, defaults)
|
||||
}
|
||||
|
||||
export function waitForAiAssistanceTextToolsMutationCalls() {
|
||||
return Mocks.waitForGraphQLMockCalls<Types.AiAssistanceTextToolsMutation>(Operations.AiAssistanceTextToolsDocument)
|
||||
}
|
||||
|
||||
export function mockAiAssistanceTextToolsMutationError(message: string, extensions: {type: ErrorTypes.GraphQLErrorTypes }) {
|
||||
return Mocks.mockGraphQLResultWithError(Operations.AiAssistanceTextToolsDocument, message, extensions);
|
||||
}
|
||||
|
|
@ -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<Array<UserError>>;
|
||||
/** Returned text */
|
||||
output?: Maybe<Scalars['String']['output']>;
|
||||
};
|
||||
|
||||
/** 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<Scalars['String']['output']>;
|
||||
};
|
||||
|
||||
/** 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<AdminPasswordAuthSendPayload>;
|
||||
/** Verify admin password authentication */
|
||||
adminPasswordAuthVerify?: Maybe<AdminPasswordAuthVerifyPayload>;
|
||||
/** Run an AI text tool service on the supplied text or HTML content */
|
||||
aiAssistanceTextTools?: Maybe<AiAssistanceTextToolsPayload>;
|
||||
/** Create a new email channel. This does not perform email validation. */
|
||||
channelEmailAdd?: Maybe<ChannelEmailAddPayload>;
|
||||
/** 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<number> | 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;
|
||||
}>;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
24
app/graphql/gql/mutations/ai_assistance/text_tools.rb
Normal file
24
app/graphql/gql/mutations/ai_assistance/text_tools.rb
Normal file
|
|
@ -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
|
||||
15
app/graphql/gql/types/enum/ai_text_tool_service_type.rb
Normal file
15
app/graphql/gql/types/enum/ai_text_tool_service_type.rb
Normal file
|
|
@ -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
|
||||
|
|
@ -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.",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,5 @@
|
|||
# Copyright (C) 2012-2025 Zammad Foundation, https://zammad-foundation.org/
|
||||
|
||||
class Controllers::AIAssistanceControllerPolicy < Controllers::ApplicationControllerPolicy
|
||||
default_permit!('ticket.agent')
|
||||
end
|
||||
36
app/services/service/ai_assistance/text_tools.rb
Normal file
36
app/services/service/ai_assistance/text_tools.rb
Normal file
|
|
@ -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
|
||||
13
config/routes/ai_assistance.rb
Normal file
13
config/routes/ai_assistance.rb
Normal file
|
|
@ -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
|
||||
47
db/migrate/20250507155325_add_ai_assistance_text_tools.rb
Normal file
47
db/migrate/20250507155325_add_ai_assistance_text_tools.rb
Normal file
|
|
@ -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
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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'),
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
|
|
|||
373
i18n/zammad.pot
373
i18n/zammad.pot
File diff suppressed because it is too large
Load diff
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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}",
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
19
lib/ai/service/prompts/system/text_expand.txt.erb
Normal file
19
lib/ai/service/prompts/system/text_expand.txt.erb
Normal file
|
|
@ -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.
|
||||
26
lib/ai/service/prompts/system/text_improve_writing.txt.erb
Normal file
26
lib/ai/service/prompts/system/text_improve_writing.txt.erb
Normal file
|
|
@ -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.
|
||||
12
lib/ai/service/prompts/system/text_simplify.txt.erb
Normal file
12
lib/ai/service/prompts/system/text_simplify.txt.erb
Normal file
|
|
@ -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.
|
||||
|
|
@ -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.
|
||||
1
lib/ai/service/prompts/user/text_expand.txt.erb
Normal file
1
lib/ai/service/prompts/user/text_expand.txt.erb
Normal file
|
|
@ -0,0 +1 @@
|
|||
<%= @context_data[:input] %>
|
||||
1
lib/ai/service/prompts/user/text_improve_writing.txt.erb
Normal file
1
lib/ai/service/prompts/user/text_improve_writing.txt.erb
Normal file
|
|
@ -0,0 +1 @@
|
|||
<%= @context_data[:input] %>
|
||||
1
lib/ai/service/prompts/user/text_simplify.txt.erb
Normal file
1
lib/ai/service/prompts/user/text_simplify.txt.erb
Normal file
|
|
@ -0,0 +1 @@
|
|||
<%= @context_data[:input] %>
|
||||
|
|
@ -1 +1 @@
|
|||
<%= @context_data[:source] %>
|
||||
<%= @context_data[:input] %>
|
||||
19
lib/ai/service/text_expand.rb
Normal file
19
lib/ai/service/text_expand.rb
Normal file
|
|
@ -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
|
||||
19
lib/ai/service/text_improve_writing.rb
Normal file
19
lib/ai/service/text_improve_writing.rb
Normal file
|
|
@ -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
|
||||
19
lib/ai/service/text_simplify.rb
Normal file
19
lib/ai/service/text_simplify.rb
Normal file
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -698,6 +698,8 @@
|
|||
</symbol><symbol id="icon-small-dot" viewBox="0 0 16 16">
|
||||
<title>small-dot</title>
|
||||
<circle cx="8" cy="8" r="3" fill-rule="evenodd"/>
|
||||
</symbol><symbol id="icon-smart-assist-elaborate" viewBox="0 0 20 20">
|
||||
<path d="m12.593 7.593-1 4-1-4-4-1 4-1 1-4 1 4 4 1-4 1ZM14.706 3.984l3.191-.399c.115-.014.118-.179.004-.198l-2.253-.375a.1.1 0 0 1-.082-.082L15.191.677c-.02-.114-.184-.11-.198.004l-.4 3.191a.1.1 0 0 0 .113.112ZM.593 11.093a.5.5 0 0 1 .5-.5h7a.5.5 0 0 1 0 1h-7a.5.5 0 0 1-.5-.5ZM.593 14.093a.5.5 0 0 1 .5-.5h15a.5.5 0 1 1 0 1h-15a.5.5 0 0 1-.5-.5ZM.593 17.093a.5.5 0 0 1 .5-.5h6a.5.5 0 0 1 0 1h-6a.5.5 0 0 1-.5-.5Z"/>
|
||||
</symbol><symbol id="icon-smart-assist" viewBox="0 0 16 16">
|
||||
<path d="M9.257 9.373 8.155 13.78c-.04.162-.27.162-.31 0L6.743 9.373a.16.16 0 0 0-.116-.116L2.22 8.155c-.162-.04-.162-.27 0-.31l4.406-1.102a.16.16 0 0 0 .116-.116L7.845 2.22c.04-.162.27-.162.31 0l1.102 4.406a.16.16 0 0 0 .116.116l4.406 1.102c.162.04.162.27 0 .31L9.373 9.257a.16.16 0 0 0-.116.116ZM3.898 12.013l-2.554.319c-.091.011-.094.143-.003.158l1.803.3a.08.08 0 0 1 .065.066l.3 1.803c.016.09.148.088.159-.004l.32-2.553a.08.08 0 0 0-.09-.09ZM12.102 12.013l2.553.319c.092.011.095.143.004.158l-1.803.3a.08.08 0 0 0-.066.066l-.3 1.803c-.015.09-.147.088-.158-.004l-.32-2.553a.08.08 0 0 1 .09-.09ZM3.898 3.987l-2.554-.319c-.091-.011-.094-.143-.003-.158l1.803-.3a.08.08 0 0 0 .065-.066l.3-1.803c.016-.09.148-.088.159.004l.32 2.553a.08.08 0 0 1-.09.09ZM12.102 3.987l2.553-.319c.092-.011.095-.143.004-.158l-1.803-.3a.08.08 0 0 1-.066-.066l-.3-1.803c-.015-.09-.147-.088-.158.004l-.32 2.553a.08.08 0 0 0 .09.09Z"/>
|
||||
</symbol><symbol id="icon-sms" viewBox="0 0 17 17">
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 115 KiB After Width: | Height: | Size: 115 KiB |
8
public/assets/images/icons/smart-assist-elaborate.svg
Normal file
8
public/assets/images/icons/smart-assist-elaborate.svg
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="20px" height="20px" viewBox="0 0 20 20" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<path d="M12.5931 7.59314L11.5931 11.5931L10.5931 7.59314L6.59314 6.59314L10.5931 5.59314L11.5931 1.59314L12.5931 5.59314L16.5931 6.59314L12.5931 7.59314Z" fill="#50E3C2" />
|
||||
<path d="M14.7056 3.98408L17.8973 3.58512C18.0117 3.57082 18.0151 3.40621 17.9013 3.38725L15.6484 3.01177C15.6063 3.00475 15.5733 2.97172 15.5662 2.92957L15.1908 0.676701C15.1718 0.562964 15.0072 0.566322 14.9929 0.680738L14.5939 3.87245C14.5858 3.93716 14.6409 3.99217 14.7056 3.98408Z" fill="#50E3C2" />
|
||||
<path d="M0.59314 11.0931C0.59314 10.817 0.816997 10.5931 1.09314 10.5931H8.09314C8.36928 10.5931 8.59314 10.817 8.59314 11.0931C8.59314 11.3693 8.36928 11.5931 8.09314 11.5931H1.09314C0.816997 11.5931 0.59314 11.3693 0.59314 11.0931Z" fill="#50E3C2" />
|
||||
<path d="M0.59314 14.0931C0.59314 13.817 0.816997 13.5931 1.09314 13.5931H16.0931C16.3693 13.5931 16.5931 13.817 16.5931 14.0931C16.5931 14.3693 16.3693 14.5931 16.0931 14.5931H1.09314C0.816997 14.5931 0.59314 14.3693 0.59314 14.0931Z" fill="#50E3C2" />
|
||||
<path d="M0.59314 17.0931C0.59314 16.817 0.816997 16.5931 1.09314 16.5931H7.09314C7.36928 16.5931 7.59314 16.817 7.59314 17.0931C7.59314 17.3693 7.36928 17.5931 7.09314 17.5931H1.09314C0.816997 17.5931 0.59314 17.3693 0.59314 17.0931Z" fill="#50E3C2" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
41
spec/graphql/gql/mutations/ai_assistance/text_tools_spec.rb
Normal file
41
spec/graphql/gql/mutations/ai_assistance/text_tools_spec.rb
Normal file
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
]
|
||||
|
|
|
|||
49
spec/requests/ai_assistance_spec.rb
Normal file
49
spec/requests/ai_assistance_spec.rb
Normal file
|
|
@ -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
|
||||
39
spec/services/service/ai_assistance/text_tools_spec.rb
Normal file
39
spec/services/service/ai_assistance/text_tools_spec.rb
Normal file
|
|
@ -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
|
||||
93
spec/system/ticket/text_tools_spec.rb
Normal file
93
spec/system/ticket/text_tools_spec.rb
Normal file
|
|
@ -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
|
||||
|
|
@ -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 <OPEN_AI_TOKEN>
|
||||
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 <OPEN_AI_TOKEN>
|
||||
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 <OPEN_AI_TOKEN>
|
||||
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
|
||||
|
|
|
|||
|
|
@ -1,20 +1,89 @@
|
|||
---
|
||||
http_interactions:
|
||||
- request:
|
||||
method: get
|
||||
uri: "<ZAMMAD_AI_API_URL>/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 <ZAMMAD_AI_TOKEN>
|
||||
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: "<ZAMMAD_AI_API_URL>/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
|
||||
|
|
|
|||
Loading…
Reference in a new issue