Search for matches and display a hint

This commit is contained in:
Artur
2026-01-19 15:37:43 +03:00
parent 4dde2ff20e
commit e06e80bb53
8 changed files with 173 additions and 109 deletions

View File

@ -56,12 +56,12 @@
<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/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/assistant-correction.js" defer></script>
<script type="text/javascript" src="scripts/custom-annotations/assistant-hint.js" defer></script>
<script type="text/javascript" src="scripts/custom-annotations/assistant-replacement.js" defer></script>
<script type="text/javascript" src="scripts/custom-annotations/custom-annotator.js"></script>
<script type="text/javascript" src="scripts/custom-annotations/annotation-popup.js"></script>
<script type="text/javascript" src="scripts/custom-annotations/assistant.js"></script>
<script type="text/javascript" src="scripts/custom-annotations/assistant-correction.js"></script>
<script type="text/javascript" src="scripts/custom-annotations/assistant-hint.js"></script>
<script type="text/javascript" src="scripts/custom-annotations/assistant-replacement.js"></script>
<script type="text/javascript" src="scripts/generate.js"></script>
<script type="text/javascript" src="scripts/code.js"></script>

View File

@ -26,7 +26,7 @@ ol {
.custom_assistant_window {
display: flex;
flex-direction: column;
margin: 12px;
margin: 2px 12px 12px 12px;
flex: 1;
width: calc(100% - 24px);
}

View File

@ -30,6 +30,10 @@
*
*/
// @ts-check
/// <reference path="./types.js" />
function CustomAnnotationPopup()
{
this.popup = null;
@ -41,6 +45,13 @@ function CustomAnnotationPopup()
this.width = 318;
this.height = 500;
/**
* @param {number} type
* @param {string} paraId
* @param {string} rangeId
* @param {InfoForPopup} data
* @returns
*/
this.open = function(type, paraId, rangeId, data)
{
this._calculateWindowSize(data);
@ -137,13 +148,17 @@ function CustomAnnotationPopup()
this._getButtons = function()
{
const buttons = [{ text: window.Asc.plugin.tr('Accept'), primary: true }];
if (this.type === 1 || this.type === 2) {
const buttons = [];
if (this.type === 0) {
buttons.push({ text: window.Asc.plugin.tr('OK'), primary: true });
} else {
buttons.push({ text: window.Asc.plugin.tr('Accept'), primary: true });
buttons.push({ text: window.Asc.plugin.tr('Reject'), primary: false });
}
return buttons;
};
/** @param {InfoForPopup} data */
this._calculateWindowSize = function(data)
{
let backColor = window.Asc.plugin.theme ? window.Asc.plugin.theme["background-normal"] : "#FFFFFF";
@ -151,6 +166,12 @@ function CustomAnnotationPopup()
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";
if (data.reason) {
this.content = `<div>
<div class="ballon-color text-color border-color" style="font-size:12px; color:${textColor}; line-height:1.5; padding:10px;">${data.reason}</div>
</div>`;
}
if (data.suggested) {
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;">

View File

@ -30,6 +30,10 @@
*
*/
/// <reference path="./custom-annotator.js" />
/// <reference path="./types.js" />
/** @param {localStorageCustomAssistantItem} assistantData */
function AssistantHint(assistantData)
{
CustomAnnotator.call(this);
@ -40,12 +44,19 @@ function AssistantHint(assistantData)
AssistantHint.prototype = Object.create(CustomAnnotator.prototype);
AssistantHint.prototype.constructor = AssistantHint;
/**
*
* @param {string} paraId
* @param {string} recalcId
* @param {string} text
* @returns
*/
AssistantHint.prototype.annotateParagraph = async function(paraId, recalcId, text)
{
this.paragraphs[paraId] = {};
let requestEngine = AI.Request.create(AI.ActionType.Chat);
if (!requestEngine)
if (!requestEngine || text.length === 0)
return false;
let isSendedEndLongAction = false;
@ -55,90 +66,15 @@ AssistantHint.prototype.annotateParagraph = async function(paraId, recalcId, tex
isSendedEndLongAction = true;
}
let argPrompt = `${this.assistantData.query}
Response format - return ONLY this JSON array with no additional text:
[
{
"origin": "relevant snippet of text around the error",
"suggestion": "the corrected version of that snippet",
"description": "brief explanation of the punctuation or style issue",
"difference":"difference between origin and suggestion"
"occurrence": 1,
"confidence": 0.95
}
]
Guidelines for each field:
- "origin": VERY SHORT SNIPPET (3-8 words) of EXACT UNCHANGED original text around the error. Do not fix anything in this field.
- "suggestion": The corrected version of that same snippet
- "difference": The difference between origin and suggestion in html format: the differences wrapped with <strong> tag
- "description": Brief explanation of the punctuation or style issue
- "occurrence": Which occurrence of this sentence if it appears multiple times (1 for first, 2 for second, etc.)
- "confidence": Value between 0 and 1 indicating certainty (1.0 = completely certain, 0.5 = uncertain)
Only include sentences that have punctuation or style errors - skip sentences with no errors.
If no errors are found in the entire text, return an empty array: []
Examples:
Input: "She dont like apples Me and him goes to school however they enjoy learning. Its a beautiful day"
Output:
[
{
"origin": "apples Me and him",
"suggestion": "apples. Me and him",
"difference": "apples<strong>.</strong> Me and him"
"description": "Missing period between sentences",
"occurrence": 1,
"confidence": 1.0
},
{
"origin": "school however they",
"suggestion": "school; however, they",
"difference": "school<strong>;</strong> however<strong>,</strong> they"
"description": "Incorrect punctuation with 'however' - should use semicolon before and comma after",
"occurrence": 1,
"confidence": 0.95
},
{
"origin": "beautiful day",
"suggestion": "beautiful day.",
"difference": "beautiful day<strong>.</strong>",
"description": "Missing period at end of sentence",
"occurrence": 1,
"confidence": 1.0
}
]
Input: "The sun is shining. however, it might rain later."
Output:
[
{
"origin": "shining. however, it",
"suggestion": "shining. However, it",
"difference": "shining. <strong>H</strong>owever, it",
"description": "Sentence should start with a capital letter",
"occurrence": 1,
"confidence": 1.0
}
]
CRITICAL - Output Format:
- Return ONLY the raw JSON array, nothing else
- DO NOT wrap the response in markdown code blocks (no \`\`\`json or \`\`\`)
- DO NOT include any explanatory text before or after the JSON
- DO NOT use escaped newlines (\\n) - return the JSON on a single line if possible
- The response should start with [ and end with ]
Text to check:`;
argPrompt += text;
const argPrompt = this._createPrompt(text);
let response = "";
await requestEngine.chatRequest(argPrompt, false, async function (data)
await requestEngine.chatRequest(argPrompt, false, async function (/** @type {string} */data)
{
if (!data)
if (!data) {
console.error('no data');
return;
}
await checkEndAction();
response += data;
@ -149,11 +85,16 @@ Text to check:`;
let ranges = [];
let _t = this;
function convertToRanges(text, corrections)
/**
* @param {string} text
* @param {Array<HintAiResponse>} matches
*/
function convertToRanges(text, matches)
{
for (const { origin, suggestion, difference, description, occurrence, confidence } of corrections)
for (const { origin, reason, paragraph, occurrence, confidence } of matches)
{
if (origin === suggestion || confidence <= 0.7)
if (confidence <= 0.7)
continue;
let count = 0;
@ -174,9 +115,7 @@ Text to check:`;
});
_t.paragraphs[paraId][rangeId] = {
"original" : origin,
"suggestion" : suggestion,
"difference" : difference,
"description" : description
"reason" : reason,
};
++rangeId;
break;
@ -201,27 +140,81 @@ Text to check:`;
catch (e)
{ }
}
AssistantHint.prototype._createPrompt = function(text) {
return `You are a text analysis specialist. Your task is to find text fragments that match the user's criteria.
MANDATORY RULES:
1. Analyze ONLY the provided text.
2. Find words, phrases, or sentences that match the user's criteria.
3. For EACH match you find:
- Provide the exact quote.
- Explain WHY it matches the criteria.
- Provide position information (paragraph number).
4. If no matches are found, return an empty array: [].
5. Format your response STRICTLY in JSON format.
Response format - return ONLY this JSON array with no additional text:
[
{
"origin": "exact text fragment that matches the query",
"reason": "detailed explanation why it matches the criteria",
"paragraph": paragraph_number,
"occurrence": 1,
"confidence": 0.95
}
]
Guidelines for each field:
- "origin": EXACT UNCHANGED original text fragment. Do not fix anything in this field.
- "reason": Clear explanation of why this fragment matches the criteria.
- "paragraph": Paragraph number where the fragment is found (1-based index)
- "occurrence": Which occurrence of this sentence if it appears multiple times (1 for first, 2 for second, etc.)
- "confidence": Value between 0 and 1 indicating certainty (1.0 = completely certain, 0.5 = uncertain)
CRITICAL - Output Format:
- Return ONLY the raw JSON array, nothing else
- DO NOT wrap the response in markdown code blocks (no \`\`\`json or \`\`\`)
- DO NOT include any explanatory text before or after the JSON
- DO NOT use escaped newlines (\\n) - return the JSON on a single line if possible
- The response should start with [ and end with ]
USER REQUEST: ${this.assistantData.query}
TEXT TO ANALYZE:
"""
${text}
"""
Please analyze this text and find all fragments that match the user's request. Be thorough but precise.`;
}
/**
* @param {string} paraId
* @param {string} rangeId
* @return {HintInfoForPopup}
*/
AssistantHint.prototype.getInfoForPopup = function(paraId, rangeId)
{
let _s = this.getAnnotation(paraId, rangeId);
return {
suggested : _s["difference"],
original : _s["original"],
explanation : _s["description"]
reason : _s["reason"]
};
};
/**
* @param {string} paraId
* @param {string} rangeId
*/
AssistantHint.prototype.onAccept = async function(paraId, rangeId)
{
let text = this.getAnnotation(paraId, rangeId)["suggestion"];
await Asc.Editor.callMethod("StartAction", ["GroupActions"]);
let range = this.getAnnotationRangeObj(paraId, rangeId);
await Asc.Editor.callMethod("SelectAnnotationRange", [range]);
Asc.scope.text = text;
await Asc.Editor.callCommand(function(){
Api.ReplaceTextSmart([Asc.scope.text]);
Api.GetDocument().RemoveSelection();
});
@ -229,6 +222,11 @@ AssistantHint.prototype.onAccept = async function(paraId, rangeId)
await Asc.Editor.callMethod("EndAction", ["GroupActions"]);
await Asc.Editor.callMethod("FocusEditor");
};
/**
* @param {string} paraId
* @param {string} rangeId
*/
AssistantHint.prototype.getAnnotationRangeObj = function(paraId, rangeId)
{
return {

View File

@ -32,10 +32,15 @@
// @ts-check
/// <reference path="./types.js" />
/// <reference path="./assistant-hint.js" />
/// <reference path="./assistant-replacement.js" />
/// <reference path="./assistant-correction.js" />
/**
* @param {localStorageCustomAssistantItem} assistantData
* @returns
*/
function createCustomAssistant(assistantData)
{
switch(assistantData.type) {

View File

@ -30,6 +30,8 @@
*
*/
/// <reference path="./types.js" />
function CustomAnnotator()
{
this.paraId = null;
@ -90,8 +92,11 @@ CustomAnnotator.prototype.openPopup = async function(paraId, rangeId)
{
if (!customAnnotationPopup)
return;
/** @type {InfoForPopup} */
const popupInfo = this.getInfoForPopup(paraId, rangeId);
let popup = customAnnotationPopup.open(this.type, paraId, rangeId, this.getInfoForPopup(paraId, rangeId));
let popup = customAnnotationPopup.open(this.type, paraId, rangeId, popupInfo);
if (!popup)
return;

View File

@ -0,0 +1,39 @@
/**
* @typedef {Object} localStorageCustomAssistantItem
* @property {string} id
* @property {string} name
* @property {number} type
* @property {string} query
*/
/**
* @typedef {Object} HintAiResponse
* @property {string} origin
* @property {string} reason
* @property {number} paragraph
* @property {number} occurrence
* @property {number} confidence
*/
/**
* @typedef {Object} CorrectionInfoForPopup
* @property {string} original
* @property {string} explanation
*/
/**
* @typedef {Object} HintInfoForPopup
* @property {string} original
* @property {string} reason
*/
/**
* @typedef {Object} ReplacementInfoForPopup
* @property {string} original
* @property {string} explanation
*/
/**
* @typedef {ReplacementInfoForPopup | HintInfoForPopup | CorrectionInfoForPopup} InfoForPopup
*/

View File

@ -34,16 +34,12 @@
/// <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
*/
/// <reference path="./custom-annotations/types.js" />
(function (window) {
const LOCAL_STORAGE_KEY = "onlyoffice_ai_saved_assistants";
/** @type {any} */
let selectType = null;
const { form, textarea, inputId, inputName } = initFormElements();
const mainContainer = document.getElementById("custom_assistant_window");