select type in custom assistant

This commit is contained in:
Artur
2026-01-16 17:46:32 +03:00
parent bebe7794d8
commit adcc75e975
6 changed files with 422 additions and 31 deletions

View File

@ -37,7 +37,10 @@
<script type="text/javascript" src="https://onlyoffice.github.io/sdkjs-plugins/v1/plugins.js"></script>
<script type="text/javascript" src="https://onlyoffice.github.io/sdkjs-plugins/v1/plugins-ui.js"></script>
<link rel="stylesheet" href="https://onlyoffice.github.io/sdkjs-plugins/v1/plugins.css">
<link rel="stylesheet" href="vendor/select2-4.0.6-rc.1/dist/css/select2.css"/>
<link rel="stylesheet" href="./resources/styles/customAssistant.css">
<script src="vendor/jquery/jquery-3.7.1.min.js"></script>
<script src="vendor/select2-4.0.6-rc.1/dist/js/select2.js"></script>
<script type="text/javascript" src="scripts/utils/theme.js" defer></script>
<script type="text/javascript" src="scripts/customAssistant.js" defer></script>
</head>
@ -52,6 +55,7 @@
<input type="hidden" id="input_prompt_id" value="" />
<input type="text" id="input_prompt_name" class="form-control i18n" maxlength="30"
placeholder="Assistant's name" spellcheck="false" required />
<select id="assistantType" class="form-control" title="Type"></select>
<textarea id="input_prompt" rows="1" class="form-control i18n" placeholder="Enter your query here..."
spellcheck="false" required></textarea>
</form>

View File

@ -56,7 +56,9 @@
<script type="text/javascript" src="scripts/text-annotations/text-annotator.js"></script>
<script type="text/javascript" src="scripts/text-annotations/spelling.js"></script>
<script type="text/javascript" src="scripts/text-annotations/grammar.js"></script>
<script type="text/javascript" src="scripts/text-annotations/custom-assistant.js"></script>
<script type="text/javascript" src="scripts/custom-annotations/annotation-popup.js" defer></script>
<script type="text/javascript" src="scripts/custom-annotations/assistant.js" defer></script>
<script type="text/javascript" src="scripts/custom-annotations/custom-annotator.js"></script>
<script type="text/javascript" src="scripts/generate.js"></script>
<script type="text/javascript" src="scripts/code.js"></script>

View File

@ -0,0 +1,204 @@
/*
* (c) Copyright Ascensio System SIA 2010-2025
*
* This program is a free software product. You can redistribute it and/or
* modify it under the terms of the GNU Affero General Public License (AGPL)
* version 3 as published by the Free Software Foundation. In accordance with
* Section 7(a) of the GNU AGPL its Section 15 shall be amended to the effect
* that Ascensio System SIA expressly excludes the warranty of non-infringement
* of any third-party rights.
*
* This program is distributed WITHOUT ANY WARRANTY; without even the implied
* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. For
* details, see the GNU AGPL at: http://www.gnu.org/licenses/agpl-3.0.html
*
* You can contact Ascensio System SIA at 20A-6 Ernesta Birznieka-Upish
* street, Riga, Latvia, EU, LV-1050.
*
* The interactive user interfaces in modified source and object code versions
* of the Program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU AGPL version 3.
*
* Pursuant to Section 7(b) of the License you must retain the original Product
* logo when distributing the program. Pursuant to Section 7(e) we decline to
* grant you any rights under trademark law for use of our trademarks.
*
* All the Product's GUI elements, including illustrations and icon sets, as
* well as technical writing content are licensed under the terms of the
* Creative Commons Attribution-ShareAlike 4.0 International. See the License
* terms at http://creativecommons.org/licenses/by-sa/4.0/legalcode
*
*/
function CustomAnnotationPopup()
{
this.popup = null;
this.type = 0; // 0 - spelling, 1 - grammar
this.paraId = -1;
this.rangeId = -1;
this.content = "";
this.width = 318;
this.height = 500;
this.open = function(type, paraId, rangeId, data)
{
if (this.popup && 0 === this.type && 1 === type)
return null;
this._calculateWindowSize(data);
return this._open(type, paraId, rangeId);
};
this._open = function(type, paraId, rangeId)
{
if (this.type === type
&& rangeId === this.rangeId
&& paraId === this.paraId)
return this.popup;
this.type = type;
this.paraId = paraId;
this.rangeId = rangeId;
if (this.popup)
this.popup.close();
let variation = {
url : 'annotationPopup.html',
isVisual : true,
buttons : this._getButtons(),
isModal : false,
description: this._getTitle(),
EditorsSupport : ["word", "slide", "cell", "pdf"],
size : [this.width, this.height],
fixedSize : true,
isTargeted : true
};
let popup = new window.Asc.PluginWindow();
let _t = this;
popup.attachEvent("onWindowReady", function() {
let name2color = {
"theme-light": "#F62211",
"theme-classic-light": "#D9534F",
"theme-dark": "#F62211",
"theme-contrast-dark": "#F62211",
"theme-gray": "#F62211",
"theme-white": "#F23D3D",
"theme-night": "#F23D3D"
};
let type2color = {
"light": "#F62211",
"dark": "#F62211"
};
let color = type2color["light"];
if (window.Asc.plugin.theme)
{
if (window.Asc.plugin.theme.Name && name2color[window.Asc.plugin.theme.Name])
color = name2color[window.Asc.plugin.theme.Name];
else if (window.Asc.plugin.theme.Type && type2color[window.Asc.plugin.theme.Type])
color = type2color[window.Asc.plugin.theme.Type];
}
popup.command("onUpdateContent", {
content : _t.content,
color : color
});
});
popup.show(variation);
this.popup = popup;
return popup;
};
this.close = function(type)
{
if (undefined !== type && this.type !== type)
return;
if (!this.popup)
return;
this.type = -1;
this.rangeId = -1;
this.paraId = -1;
this.popup.close();
this.popup = null;
Asc.Editor.callMethod("FocusEditor");
};
this._getTitle = function()
{
return window.Asc.plugin.tr(this.type === 0 ? "Spelling suggestion" : "Grammar suggestion");
};
this._getButtons = function()
{
return [
{ text: window.Asc.plugin.tr('Accept'), primary: true },
{ text: window.Asc.plugin.tr('Reject'), primary: false }
];
};
this._calculateWindowSize = function(data)
{
let backColor = window.Asc.plugin.theme ? window.Asc.plugin.theme["background-normal"] : "#FFFFFF";
let textColor = window.Asc.plugin.theme ? window.Asc.plugin.theme["text-normal"] : "#3D3D3D";
let borderColor = window.Asc.plugin.theme ? window.Asc.plugin.theme["border-divider"] : "#666666";
let ballonColor = window.Asc.plugin.theme ? window.Asc.plugin.theme["canvas-background"] : "#F5F5F5";
this.content = `<div class="back-color text-color" style="background:${backColor}; overflow:hidden; max-width:320px; min-width:280px;color:${textColor}; user-select:none;font-family:-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;">
<div style="padding:16px 16px 0px 16px;">
<div style="margin-bottom:12px;">
<div class="text-color" style="font-size:11px; font-weight:700; color:${textColor}; margin-bottom:6px;">
${window.Asc.plugin.tr("Suggested correction")}
</div>
<div class="ballon-color text-color border-color" style="font-size:12px; color:${textColor}; line-height:1.5; background:${ballonColor}; border:1px solid ${borderColor}; border-radius:3px; padding:10px;">
<div style="display:flex; align-items:center; gap:8px;">
<span class="text-color" style="color:${textColor}; font-weight:normal;">${data.original}</span>
<span class="text-color" style="color:${textColor}; font-weight:bold;">→</span>
<span class="text-color" style="color:${textColor}; font-weight:normal;">${data.suggested}</span>
</div>
</div>
</div>`;
if (data.explanation) {
this.content += `<div style="margin-bottom:16px;">
<div class="text-color" class="text-color" style="font-size:11px; font-weight:700; color:${textColor}; margin-bottom:6px;">
${window.Asc.plugin.tr("Explanation")}
</div>
<div class="ballon-color text-color border-color" style="font-size:12px; color:${textColor}; line-height:1.5; background:${ballonColor}; border:1px solid ${borderColor}; border-radius:3px; padding:10px;">${data.explanation}</div>
</div>`;
}
this.content += "</div></div>";
let measureDiv = document.createElement("div");
measureDiv.style.position = "absolute";
measureDiv.style.left = "-9999px";
measureDiv.style.top = "-9999px";
measureDiv.style.width = this.width + "px";
measureDiv.style.visibility = "hidden";
measureDiv.style.pointerEvents = "none";
measureDiv.style.opacity = "0";
measureDiv.style.margin = "0";
measureDiv.style.padding = "0";
measureDiv.innerHTML = this.content;
document.body.appendChild(measureDiv);
this.height = measureDiv.scrollHeight;
document.body.removeChild(measureDiv);
};
}
var customAnnotationPopup = new CustomAnnotationPopup();

View File

@ -32,12 +32,12 @@
function CustomAssistant(assistantData)
{
TextAnnotator.call(this);
CustomAnnotator.call(this);
this.type = 1;
this.assistantData = assistantData;
}
CustomAssistant.prototype = Object.create(TextAnnotator.prototype);
CustomAssistant.prototype = Object.create(CustomAnnotator.prototype);
CustomAssistant.prototype.constructor = CustomAssistant;
CustomAssistant.prototype.annotateParagraph = async function(paraId, recalcId, text)
@ -55,28 +55,7 @@ CustomAssistant.prototype.annotateParagraph = async function(paraId, recalcId, t
isSendedEndLongAction = true;
}
let argPrompt = `You are a grammar correction tool that analyzes text for punctuation and style issues only. You will receive text to analyze and must respond with corrections in a specific JSON format.
CRITICAL REQUIREMENT - READ CAREFULLY:
The "sentence" field in your JSON response MUST contain the EXACT text from the original input with NO changes whatsoever - not even fixing capitalization, punctuation, or anything else. Copy it character-by-character exactly as it appears in the original. Only the "suggestion" field should contain corrections.
Your task is to:
- Check ONLY for punctuation errors (commas, periods, semicolons, colons, apostrophes, quotation marks, etc.) and style issues (sentence structure, word order, grammar, capitalization)
- Completely ignore spelling errors and typos. Do not mention them, do not flag them, do not include sentences just because they contain spelling errors. Pretend all words are spelled correctly.
- Return corrections in JSON format only
What counts as an error:
- Missing or incorrect punctuation (periods, commas, semicolons, etc.)
- Run-on sentences needing punctuation
- Incorrect sentence structure or word order
- Grammar issues (subject-verb agreement, tense consistency, etc.)
- Capitalization errors
What does NOT count as an error:
- Misspelled words or typos
- Missing letters in words
- Wrong letters in words
let argPrompt = `${this.assistantData.query}
Response format - return ONLY this JSON array with no additional text:
[
{

View File

@ -0,0 +1,175 @@
/*
* (c) Copyright Ascensio System SIA 2010-2025
*
* This program is a free software product. You can redistribute it and/or
* modify it under the terms of the GNU Affero General Public License (AGPL)
* version 3 as published by the Free Software Foundation. In accordance with
* Section 7(a) of the GNU AGPL its Section 15 shall be amended to the effect
* that Ascensio System SIA expressly excludes the warranty of non-infringement
* of any third-party rights.
*
* This program is distributed WITHOUT ANY WARRANTY; without even the implied
* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. For
* details, see the GNU AGPL at: http://www.gnu.org/licenses/agpl-3.0.html
*
* You can contact Ascensio System SIA at 20A-6 Ernesta Birznieka-Upish
* street, Riga, Latvia, EU, LV-1050.
*
* The interactive user interfaces in modified source and object code versions
* of the Program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU AGPL version 3.
*
* Pursuant to Section 7(b) of the License you must retain the original Product
* logo when distributing the program. Pursuant to Section 7(e) we decline to
* grant you any rights under trademark law for use of our trademarks.
*
* All the Product's GUI elements, including illustrations and icon sets, as
* well as technical writing content are licensed under the terms of the
* Creative Commons Attribution-ShareAlike 4.0 International. See the License
* terms at http://creativecommons.org/licenses/by-sa/4.0/legalcode
*
*/
function CustomAnnotator()
{
this.paraId = null;
this.rangeId = null;
this.paragraphs = {};
this.waitParagraphs = {};
this.paraToCheck = new Set();
this.checked = new Set(); // was checked on the previous request
this.type = -1;
}
CustomAnnotator.prototype.onChangeParagraph = async function(paraId, recalcId, text, ranges)
{
this._handleNewRanges(ranges, paraId, text);
this.waitParagraphs[paraId] = {
recalcId : recalcId,
text : text
};
this._checkParagraph(paraId);
};
CustomAnnotator.prototype.checkParagraphs = async function(paraIds)
{
this.paraToCheck.clear()
let _t = this;
paraIds.forEach(function(paraId) {
if (!_t.checked.has(paraId) || _t.waitParagraphs[paraId])
_t.paraToCheck.add(paraId);
});
this.paraToCheck.forEach(paraId => this._checkParagraph(paraId));
};
CustomAnnotator.prototype._checkParagraph = async function(paraId)
{
if (!this.paraToCheck.has(paraId) || !this.waitParagraphs[paraId])
return;
let recalcId = this.waitParagraphs[paraId].recalcId;
let text = this.waitParagraphs[paraId].text;
// TODO: Temporarily for simplicity
let range = this.getAnnotationRangeObj(paraId);
range["rangeId"] = undefined;
range["all"] = true;
await Asc.Editor.callMethod("RemoveAnnotationRange", [range]);
await this.annotateParagraph(paraId, recalcId, text);
delete this.waitParagraphs[paraId];
this.paraToCheck.delete(paraId);
this.checked.add(paraId);
};
CustomAnnotator.prototype.annotateParagraph = async function(paraId, recalcId, text)
{
};
CustomAnnotator.prototype.openPopup = async function(paraId, rangeId)
{
if (!customAnnotationPopup)
return;
let popup = customAnnotationPopup.open(this.type, paraId, rangeId, this.getInfoForPopup(paraId, rangeId));
if (!popup)
return;
let _t = this;
popup.onAccept = async function() {
await _t.onAccept(paraId, rangeId);
_t.closePopup();
};
popup.onReject = async function() {
await _t.onReject(paraId, rangeId);
_t.closePopup();
};
};
CustomAnnotator.prototype.closePopup = function()
{
if (!customAnnotationPopup)
return;
customAnnotationPopup.close(this.type);
};
CustomAnnotator.prototype.getInfoForPopup = function(paraId, rangeId)
{
return {};
};
CustomAnnotator.prototype.onAccept = async function(paraId, rangeId)
{
};
CustomAnnotator.prototype.onReject = async function(paraId, rangeId)
{
let range = this.getAnnotationRangeObj(paraId, rangeId);
await Asc.Editor.callMethod("RemoveAnnotationRange", [range]);
};
CustomAnnotator.prototype.getAnnotation = function(paraId, rangeId)
{
if (!paraId || !rangeId || !this.paragraphs[paraId] || !this.paragraphs[paraId][rangeId])
return {};
return this.paragraphs[paraId][rangeId];
};
CustomAnnotator.prototype.getAnnotationRangeObj = function(paraId, rangeId)
{
return {
"paragraphId" : paraId,
"rangeId" : rangeId
};
};
CustomAnnotator.prototype.onClick = function(paraId, ranges)
{
if (!ranges || !ranges.length)
this.closePopup();
else
this.openPopup(paraId, ranges[0]);
};
CustomAnnotator.prototype.onBlur = function()
{
this.closePopup();
this.resetCurrentRange();
};
CustomAnnotator.prototype.onFocus = function(paraId, rangeId)
{
};
CustomAnnotator.prototype.resetCurrentRange = function()
{
this.paraId = null;
this.rangeId = null;
};
CustomAnnotator.prototype._handleNewRanges = function(ranges, paraId, text)
{
if (!ranges || !Array.isArray(ranges))
return;
ranges.forEach(range => this._handleNewRangePositions(range, paraId, text));
// ↓↓↓ TODO: the cycle seems to make no sense ↓↓↓
for (let i = 0; i < ranges.length; ++i)
{
this._handleNewRangePositions(ranges[i]);
}
};
CustomAnnotator.prototype._handleNewRangePositions = function(range, paraId, text)
{
};

View File

@ -33,16 +33,19 @@
// @ts-check
/// <reference path="./utils/theme.js" />
/// <reference path="../vendor/select2-4.0.6-rc.1/dist/js/select2.js" />
/**
* @typedef {Object} localStorageCustomAssistantItem
* @property {string} id
* @property {string} name
* @property {number} type
* @property {string} query
*/
(function (window) {
const LOCAL_STORAGE_KEY = "onlyoffice_ai_saved_assistants";
const { form, textarea, inputId, inputName } = getFormElements();
let selectType = null;
const { form, textarea, inputId, inputName } = initFormElements();
const mainContainer = document.getElementById("custom_assistant_window");
if (!mainContainer) {
console.error("Custom Assistant: required elements are missing");
@ -92,6 +95,7 @@
if (assistant) {
inputId.value = assistant.id;
inputName.value = assistant.name;
selectType.val(assistant.type).trigger('change');
textarea.value = assistant.query;
}
@ -122,8 +126,9 @@
/** @returns {localStorageCustomAssistantItem} */
function saveCustomAssistantToLocalStorage() {
const id = inputId.value.trim();
const id = inputId.value;
const name = inputName.value.trim();
const type = Number(selectType.val());
const query = textarea.value.trim();
/** @type {localStorageCustomAssistantItem[]} */
@ -134,9 +139,9 @@
(item) => item.id === id
);
if (existingAssistantIndex !== -1) {
savedAssistants[existingAssistantIndex] = { id, name, query };
savedAssistants[existingAssistantIndex] = { id, name, type, query };
} else {
savedAssistants.push({ id, name, query });
savedAssistants.push({ id, name, type, query });
}
localStorage.setItem(
@ -144,11 +149,11 @@
JSON.stringify(savedAssistants)
);
return { id, name, query };
return { id, name, type, query };
}
/** @returns {{textarea: HTMLTextAreaElement, inputId: HTMLInputElement, inputName: HTMLInputElement, form: HTMLFormElement}} */
function getFormElements() {
function initFormElements() {
const form = document.getElementById("input_prompt_wrapper");
const inputId = document.getElementById("input_prompt_id");
const inputName = document.getElementById("input_prompt_name");
@ -172,6 +177,28 @@
);
}
selectType = $('#assistantType');
selectType.select2({
data : [{
id: 0,
text: "Hint"
},
{
id: 1,
text: "Replace"
}],
tags: true,
minimumResultsForSearch: Infinity,
dropdownAutoWidth: true
});
selectType.on('select2:select', (e) => {
});
selectType.val(1);
selectType.trigger('select2:select');
selectType.trigger('change');
return { form, textarea, inputId, inputName };
}