Unification of assistants

This commit is contained in:
Artur
2026-01-20 13:15:34 +03:00
parent b992ebfa43
commit 79affa543c
5 changed files with 166 additions and 181 deletions

View File

@ -166,36 +166,36 @@ function CustomAnnotationPopup()
if (data.type === 0) { // Hint
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 class="ballon-color text-color border-color" style="font-size:12px; color:${textColor}; line-height:1.5; padding:10px;">${data.explanation}</div>
</div>`;
}
} else { // Replace + Hint or Replace
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;">
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;">
<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 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>
</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>`;
<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>";

View File

@ -40,16 +40,13 @@ function AssistantHint(assistantData)
this.type = assistantData.type; // 0
this.assistantData = assistantData;
}
AssistantHint.prototype = Object.create(CustomAnnotator.prototype);
AssistantHint.prototype.constructor = AssistantHint;
/**
*
* @param {string} paraId
* @param {string} recalcId
* @param {string} text
* @returns
* @param {string} text
*/
AssistantHint.prototype.annotateParagraph = async function(paraId, recalcId, text)
{
@ -71,11 +68,9 @@ AssistantHint.prototype.annotateParagraph = async function(paraId, recalcId, tex
let response = "";
await requestEngine.chatRequest(argPrompt, false, async function (/** @type {string} */data)
{
if (!data) {
if (!data)
return;
}
await checkEndAction();
response += data;
});
await checkEndAction();
@ -103,7 +98,7 @@ AssistantHint.prototype.annotateParagraph = async function(paraId, recalcId, tex
{
const index = text.indexOf(origin, searchStart);
if (index === -1) break;
count++;
if (count === occurrence)
{
@ -114,7 +109,7 @@ AssistantHint.prototype.annotateParagraph = async function(paraId, recalcId, tex
});
_t.paragraphs[paraId][rangeId] = {
"original" : origin,
"reason" : reason,
"reason" : reason
};
++rangeId;
break;
@ -140,8 +135,13 @@ AssistantHint.prototype.annotateParagraph = async function(paraId, recalcId, tex
{ }
}
/**
* @param {string} text
* @returns {string}
*/
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.
let prompt = `You are a multi-disciplinary text analysis assistant.
Your task is to find text fragments that match the user's criteria.
MANDATORY RULES:
1. Analyze ONLY the provided text.
@ -173,7 +173,7 @@ AssistantHint.prototype._createPrompt = function(text) {
- "confidence": Value between 0 and 1 indicating certainty (1.0 = completely certain, 0.5 = uncertain)
CRITICAL
- Ouput should be in the exact this format
- Output should be in the exact this format
- No any comments are allowed
CRITICAL - Output Format:
@ -182,15 +182,14 @@ AssistantHint.prototype._createPrompt = function(text) {
- 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}
`;
prompt += "\n\nUSER REQUEST:\n```" + this.assistantData.query + "\n```\n\n";
TEXT TO ANALYZE:
"""
${text}
"""
prompt += "TEXT TO ANALYZE:\n```\n" + text + "\n```\n\n";
Please analyze this text and find all fragments that match the user's request. Be thorough but precise.`;
prompt += `Please analyze this text and find all fragments that match the user's request. Be thorough but precise.`;
return prompt;
}
/**
@ -203,7 +202,7 @@ AssistantHint.prototype.getInfoForPopup = function(paraId, rangeId)
let _s = this.getAnnotation(paraId, rangeId);
return {
original : _s["original"],
reason : _s["reason"],
explanation : _s["reason"],
type : this.type
};
};
@ -215,7 +214,7 @@ AssistantHint.prototype.getInfoForPopup = function(paraId, rangeId)
AssistantHint.prototype.onAccept = async function(paraId, rangeId)
{
await Asc.Editor.callMethod("StartAction", ["GroupActions"]);
let range = this.getAnnotationRangeObj(paraId, rangeId);
await Asc.Editor.callMethod("SelectAnnotationRange", [range]);
@ -253,7 +252,7 @@ AssistantHint.prototype._handleNewRangePositions = async function(range, paraId,
let start = range["start"];
let len = range["length"];
if (annot["original"] !== text.substring(start, start + len))
{
let annotRange = this.getAnnotationRangeObj(paraId, rangeId);

View File

@ -40,16 +40,20 @@ function AssistantReplaceHint(assistantData)
this.type = assistantData.type; // 1
this.assistantData = assistantData;
}
AssistantReplaceHint.prototype = Object.create(CustomAnnotator.prototype);
AssistantReplaceHint.prototype.constructor = AssistantReplaceHint;
/**
* @param {string} paraId
* @param {string} recalcId
* @param {string} text
*/
AssistantReplaceHint.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;
@ -59,15 +63,14 @@ AssistantReplaceHint.prototype.annotateParagraph = async function(paraId, recalc
isSendedEndLongAction = true;
}
let argPrompt = this._createPrompt(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)
return;
await checkEndAction();
response += data;
});
await checkEndAction();
@ -76,14 +79,14 @@ AssistantReplaceHint.prototype.annotateParagraph = async function(paraId, recalc
let ranges = [];
let _t = this;
/**
* @param {string} text
* @param {ReplaceHintAiResponse[]} corrections
*/
function convertToRanges(text, corrections)
{
for (const { origin, suggestion, difference, description, occurrence, confidence } of corrections)
for (const { origin, suggestion, difference, reason, paragraph, occurrence, confidence } of corrections)
{
if (origin === suggestion || confidence <= 0.7)
continue;
@ -95,7 +98,7 @@ AssistantReplaceHint.prototype.annotateParagraph = async function(paraId, recalc
{
const index = text.indexOf(origin, searchStart);
if (index === -1) break;
count++;
if (count === occurrence)
{
@ -108,7 +111,7 @@ AssistantReplaceHint.prototype.annotateParagraph = async function(paraId, recalc
"original" : origin,
"suggestion" : suggestion,
"difference" : difference,
"description" : description
"reason" : reason
};
++rangeId;
break;
@ -139,12 +142,12 @@ AssistantReplaceHint.prototype.annotateParagraph = async function(paraId, recalc
* @returns {string}
*/
AssistantReplaceHint.prototype._createPrompt = function(text) {
return `You are an intelligent text analysis and transformation assistant.
Your task is to analyze text and identify elements that match user-defined criteria for replacement.
let prompt = `You are a multi-disciplinary text analysis and transformation assistant.
Your task is to analyze text based on user's specific criteria and provide intelligent corrections.
MANDATORY RULES:
1. UNDERSTAND the user's intent from their criteria.
2. FIND all text elements matching the criteria.
2. Find words, phrases, or sentences that match the user's criteria.
3. For EACH match you find:
- Provide the exact quote.
- SUGGEST appropriate replacements.
@ -152,27 +155,14 @@ AssistantReplaceHint.prototype._createPrompt = function(text) {
- Provide position information (paragraph number).
4. If no matches are found, return an empty array: [].
5. Format your response STRICTLY in JSON format.
ANALYSIS FRAMEWORK:
For each text element, consider:
- SEMANTIC: Does it match the meaning criteria?
- STYLISTIC: Does it match the style criteria?
- CONTEXTUAL: Is it appropriate for the context?
- FUNCTIONAL: Does it serve the intended purpose?
REPLACEMENT STRATEGIES:
1. Direct synonym replacement
2. Paraphrasing for better fit
3. Complete restructuring if needed
4. Adding/removing elements as required
5. Adjusting tone or register
6. Support multiple languages (English, Russian, etc.)
Response format - return ONLY this JSON array with no additional text:
[
{
"origin": "exact text fragment that matches the query",
"suggestion": "suggested replacement",
"description": "detailed explanation why it matches the criteria",
"reason": "detailed explanation why it matches the criteria",
"difference":"difference between origin and suggestion"
"paragraph": paragraph_number,
"occurrence": 1,
@ -185,16 +175,15 @@ AssistantReplaceHint.prototype._createPrompt = function(text) {
- "suggestion": Your suggested replacement for the fragment.
* Ensure it aligns with the user's criteria.
* Maintain coherence with surrounding text.
- "description": Clear explanation of why this fragment matches the criteria.
- "reason": Clear explanation of why this fragment matches the criteria.
- "difference": The difference between origin and suggestion in html format: the differences wrapped with <strong> tag
- "paragraph": Paragraph number where the fragment is found (0-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 - Word Boundaries (MOST IMPORTANT):
- ONLY match complete, standalone words separated by spaces, punctuation, or at the start/end of text
- DO NOT match letters or substrings that are PART of other words
- A word is bounded by: spaces, punctuation (.,!?;:), quotes, or start/end of text
CRITICAL:
- Output should be in the exact this format
- No any comments are allowed
CRITICAL - Output Format:
- Return ONLY the raw JSON array, nothing else
@ -202,14 +191,14 @@ AssistantReplaceHint.prototype._createPrompt = function(text) {
- 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}
`;
prompt += "\n\nUSER REQUEST:\n```" + this.assistantData.query + "\n```\n\n";
TEXT TO ANALYZE:
"""
${text}
"""
`;
prompt += "TEXT TO ANALYZE:\n```\n" + text + "\n```\n\n";
prompt += `Please analyze this text and find all fragments that match the user's request. Be thorough but precise.`;
return prompt;
}
/**
@ -221,18 +210,23 @@ AssistantReplaceHint.prototype.getInfoForPopup = function(paraId, rangeId)
{
let _s = this.getAnnotation(paraId, rangeId);
return {
suggested : _s["difference"],
original : _s["original"],
explanation : _s["description"],
suggested : _s["difference"],
explanation : _s["reason"],
type : this.type
};
};
/**
* @param {string} paraId
* @param {string} rangeId
*/
AssistantReplaceHint.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]);
@ -246,6 +240,11 @@ AssistantReplaceHint.prototype.onAccept = async function(paraId, rangeId)
await Asc.Editor.callMethod("EndAction", ["GroupActions"]);
await Asc.Editor.callMethod("FocusEditor");
};
/**
* @param {string} paraId
* @param {string} rangeId
*/
AssistantReplaceHint.prototype.getAnnotationRangeObj = function(paraId, rangeId)
{
return {
@ -267,7 +266,7 @@ AssistantReplaceHint.prototype._handleNewRangePositions = async function(range,
let start = range["start"];
let len = range["length"];
if (annot["original"] !== text.substring(start, start + len))
{
let annotRange = this.getAnnotationRangeObj(paraId, rangeId);

View File

@ -43,12 +43,17 @@ function AssistantReplace(assistantData)
AssistantReplace.prototype = Object.create(CustomAnnotator.prototype);
AssistantReplace.prototype.constructor = AssistantReplace;
/**
* @param {string} paraId
* @param {string} recalcId
* @param {string} text
*/
AssistantReplace.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;
@ -59,8 +64,9 @@ AssistantReplace.prototype.annotateParagraph = async function(paraId, recalcId,
}
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)
return;
@ -80,9 +86,9 @@ AssistantReplace.prototype.annotateParagraph = async function(paraId, recalcId,
*/
function convertToRanges(text, corrections)
{
for (const { wrong, correct, reason, paragraph, occurrence, confidence } of corrections)
for (const { origin, suggestion, paragraph, occurrence, confidence } of corrections)
{
if (wrong === correct)
if (origin === suggestion || confidence <= 0.7)
continue;
let count = 0;
@ -90,35 +96,29 @@ AssistantReplace.prototype.annotateParagraph = async function(paraId, recalcId,
while (searchStart < text.length)
{
const index = text.indexOf(wrong, searchStart);
const index = text.indexOf(origin, searchStart);
if (index === -1) break;
const isStartBoundary = index === 0 || _t._isWordBoundary(text[index - 1]);
const isEndBoundary = index + wrong.length === text.length || _t._isWordBoundary(text[index + wrong.length]);
if (isStartBoundary && isEndBoundary)
count++;
if (count === occurrence)
{
count++;
if (count === occurrence)
{
ranges.push({
"start": index,
"length": wrong.length,
"id": rangeId
});
_t.paragraphs[paraId][rangeId] = {
"suggested" : correct,
"original" : wrong
};
++rangeId;
break;
}
ranges.push({
"start": index,
"length": origin.length,
"id": rangeId
});
_t.paragraphs[paraId][rangeId] = {
"original" : origin,
"suggestion" : suggestion,
};
++rangeId;
break;
}
searchStart = index + 1;
}
}
}
try
{
convertToRanges(text, JSON.parse(response));
@ -140,8 +140,8 @@ AssistantReplace.prototype.annotateParagraph = async function(paraId, recalcId,
* @returns {string}
*/
AssistantReplace.prototype._createPrompt = function(text) {
return `You are an intelligent text analysis and transformation assistant.
Your task is to analyze text and identify words that match user-defined criteria for replacement.
let prompt = `You are a multi-disciplinary text analysis and transformation assistant.
Your task is to analyze text based on user's specific criteria and provide intelligent corrections.
MANDATORY RULES:
1. UNDERSTAND the user's intent from their criteria.
@ -153,26 +153,13 @@ AssistantReplace.prototype._createPrompt = function(text) {
- Provide position information (paragraph number).
4. If no matches are found, return an empty array: [].
5. Format your response STRICTLY in JSON format.
ANALYSIS FRAMEWORK:
For each text element, consider:
- SEMANTIC: Does it match the meaning criteria?
- STYLISTIC: Does it match the style criteria?
- CONTEXTUAL: Is it appropriate for the context?
- FUNCTIONAL: Does it serve the intended purpose?
REPLACEMENT STRATEGIES:
1. Direct synonym replacement
2. Paraphrasing for better fit
3. Complete restructuring if needed
4. Adding/removing elements as required
5. Adjusting tone or register
6. Support multiple languages (English, Russian, etc.)
Response format - return ONLY this JSON array with no additional text:
[
{
"wrong": "exact text fragment that matches the query",
"correct": "suggested replacement",
"origin": "exact text fragment that matches the query",
"suggestion": "suggested replacement",
"paragraph": paragraph_number,
"occurrence": 1,
"confidence": 0.95
@ -180,18 +167,17 @@ AssistantReplace.prototype._createPrompt = function(text) {
]
Guidelines for each field:
- "wrong": EXACT UNCHANGED original text fragment. Do not fix anything in this field.
- "correct": Your suggested replacement for the fragment.
- "origin": EXACT UNCHANGED original text fragment. Do not fix anything in this field.
- "suggestion": Your suggested replacement for the fragment.
* Ensure it aligns with the user's criteria.
* Maintain coherence with surrounding text.
- "paragraph": Paragraph number where the fragment is found (0-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 - Word Boundaries (MOST IMPORTANT):
- ONLY match complete, standalone words separated by spaces, punctuation, or at the start/end of text
- DO NOT match letters or substrings that are PART of other words
- A word is bounded by: spaces, punctuation (.,!?;:), quotes, or start/end of text
CRITICAL:
- Output should be in the exact this format
- No any comments are allowed
CRITICAL - Output Format:
- Return ONLY the raw JSON array, nothing else
@ -199,15 +185,14 @@ AssistantReplace.prototype._createPrompt = function(text) {
- 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}
`;
prompt += "\n\nUSER REQUEST:\n```" + this.assistantData.query + "\n```\n\n";
TEXT TO ANALYZE:
"""
${text}
"""
prompt += "TEXT TO ANALYZE:\n```\n" + text + "\n```\n\n";
Please analyze this text and find all words that match the user's request. Be thorough but precise.`;
prompt += `Please analyze this text and find all fragments that match the user's request. Be thorough but precise.`;
return prompt;
}
/**
@ -217,33 +202,42 @@ AssistantReplace.prototype._createPrompt = function(text) {
*/
AssistantReplace.prototype.getInfoForPopup = function(paraId, rangeId)
{
let anot = this.getAnnotation(paraId, rangeId);
let _s = this.getAnnotation(paraId, rangeId);
return {
suggested : anot["suggested"],
original : anot["original"],
original : _s["original"],
suggested : _s["suggestion"],
type : this.type
};
};
/**
* @param {string} paraId
* @param {string} rangeId
*/
AssistantReplace.prototype.onAccept = async function(paraId, rangeId)
{
let anot = this.getAnnotation(paraId, rangeId);
if (!anot)
return;
let text = this.getAnnotation(paraId, rangeId)["suggestion"];
let range = this.getAnnotationRangeObj(paraId, rangeId);
await Asc.Editor.callMethod("StartAction", ["GroupActions"]);
let range = this.getAnnotationRangeObj(paraId, rangeId);
await Asc.Editor.callMethod("SelectAnnotationRange", [range]);
Asc.scope.text = anot["suggested"];
Asc.scope.text = text;
await Asc.Editor.callCommand(function(){
Api.ReplaceTextSmart([Asc.scope.text]);
Api.GetDocument().RemoveSelection();
});
});
await Asc.Editor.callMethod("RemoveAnnotationRange", [range]);
await Asc.Editor.callMethod("EndAction", ["GroupActions"]);
await Asc.Editor.callMethod("FocusEditor");
};
/**
* @param {string} paraId
* @param {string} rangeId
*/
AssistantReplace.prototype.getAnnotationRangeObj = function(paraId, rangeId)
{
return {
@ -265,17 +259,10 @@ AssistantReplace.prototype._handleNewRangePositions = async function(range, para
let start = range["start"];
let len = range["length"];
const isStartBoundary = start === 0 || this._isWordBoundary(text[start - 1]);
const isEndBoundary = start + len === text.length || this._isWordBoundary(text[start + len]);
if (!isStartBoundary || !isEndBoundary || annot["original"] !== text.substring(start, start + len))
if (annot["original"] !== text.substring(start, start + len))
{
let annotRange = this.getAnnotationRangeObj(paraId, rangeId);
Asc.Editor.callMethod("RemoveAnnotationRange", [annotRange]);
}
};
AssistantReplace.prototype._isWordBoundary = function(char)
{
return /[\s.,!?;:'"()\[\]{}\-–—\/\\]/.test(char);
};

View File

@ -7,20 +7,8 @@
*/
/**
* @typedef {Object} ReplaceHintAiResponse
* @typedef {Object} HintAiResponse
* @property {string} origin
* @property {string} suggestion
* @property {string} description
* @property {string} difference
* @property {number} paragraph
* @property {number} occurrence
* @property {number} confidence
*/
/**
* @typedef {Object} ReplaceAiResponse
* @property {string} wrong
* @property {string} correct
* @property {string} reason
* @property {number} paragraph
* @property {number} occurrence
@ -28,8 +16,16 @@
*/
/**
* @typedef {Object} HintAiResponse
* @typedef {Object} HintInfoForPopup
* @property {string} original
* @property {string} explanation
* @property {number} type
*/
/**
* @typedef {Object} ReplaceAiResponse
* @property {string} origin
* @property {string} suggestion
* @property {string} reason
* @property {number} paragraph
* @property {number} occurrence
@ -44,10 +40,14 @@
*/
/**
* @typedef {Object} HintInfoForPopup
* @property {string} original
* @typedef {Object} ReplaceHintAiResponse
* @property {string} origin
* @property {string} suggestion
* @property {string} reason
* @property {number} type
* @property {string} difference
* @property {number} paragraph
* @property {number} occurrence
* @property {number} confidence
*/
/**