mirror of
https://github.com/ONLYOFFICE/onlyoffice.github.io.git
synced 2026-04-07 14:04:30 +08:00
- ARTICLE.md
This commit is contained in:
@ -1,246 +0,0 @@
|
|||||||
# Text Annotations API Guide
|
|
||||||
|
|
||||||
Нужно написать техническую статью для разработчиков. Есть текстовый редактор OnlyOffice, у него есть API для разработчиков плагинов. В Api добавились новые методы для разметки, т.е. выделения участков текста. У выделенных участков появляется подчеркивание снизу.
|
|
||||||
|
|
||||||
Данный функционал планируется использовать для аннотаций к параграфам текста, поэтому и участки с текстом называются аннотациями.
|
|
||||||
|
|
||||||
Для управления аннотациями есть 3 метода:
|
|
||||||
|
|
||||||
1) *[```AnnotateParagraph```](https://api.onlyoffice.com/docs/plugin-and-macros/interacting-with-editors/text-document-api/Methods/AnnotateParagraph/)* - добавляет аннотации к указанному абзацу.
|
|
||||||
2) *[```SelectAnnotationRange```](https://api.onlyoffice.com/docs/plugin-and-macros/interacting-with-editors/text-document-api/Methods/SelectAnnotationRange/)* - выделяет текст в документе, используя заданную аннотацию.
|
|
||||||
3) *[```RemoveAnnotationRange```](https://api.onlyoffice.com/docs/plugin-and-macros/interacting-with-editors/text-document-api/Methods/RemoveAnnotationRange/)* - Удаляет определенный диапазон аннотаций из документа.
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
### О новых методах на примере написания плагина.
|
|
||||||
|
|
||||||
Инструкция как пользоваться плагином находится [тут](https://github.com/ONLYOFFICE-PLUGINS/onlyoffice.github.io/blob/feature/AI/sdkjs-plugins/content/ai/scripts/text-annotations/custom-annotations/README.md).
|
|
||||||
Плагин будет добавлять аннотации к тексту, т.е. он будет помогать пользователям создавать AI ассистентов, с помощью которых они будут анализировать текст и выделять участки, которые удовлетворяют критериям описанным в промпте AI ассистента. На выбор будет 3 опции:
|
|
||||||
|
|
||||||
1) *Hint* - просто показать пояснительный текст
|
|
||||||
2) *Replace* - предложить текст для замены
|
|
||||||
3) *Replace + Hint* - предложить текст для замены и снизу показать пояснительный текст. В пояснительном тексте могут быть ссылки.
|
|
||||||
|
|
||||||
Использовать плагин можно разными способами, думаю пользователи могут придумать намного больше вариантов, чем описано в этой статье. Например можно сделать проверку текста на плагиат, указав порог уникальности в 90%, или проверять не сгенерирован ли текст с помощью ИИ. Можно проверять ошибки, например несоответствие дат, имен или фактов. Можно проводить лексический анализ текста или смотреть соответствует ли текст гражданскому кодексу.
|
|
||||||
|
|
||||||
Интерфейс добавления нового или редактирования существующего ассистента состоит из 3 полей:
|
|
||||||
|
|
||||||
1) *Имя*
|
|
||||||
2) *Тип* (Hint, Replace, Replace + Hint)
|
|
||||||
3) *Промпт* - самое важное поле, в которое пользователь будет писать свой запрос, тут нужно быть максимально конкретным, желательно без размытых формулировок.
|
|
||||||
|
|
||||||
Еще есть скрытое поле, в котором содержится уникальный id ассистента.
|
|
||||||
|
|
||||||
Сохраняется ассистент в localStorage в виде объекта упакованного в строку:
|
|
||||||
|
|
||||||
```js
|
|
||||||
const assistant = {
|
|
||||||
id: string,
|
|
||||||
name: string,
|
|
||||||
type: number, // 0 - Hint, 1 - Replace, 2 - Replace + Hint
|
|
||||||
query: string, // prompt - пользовательский запрос
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Пример создания ассистента
|
|
||||||
|
|
||||||
- Имя: *Корректор дат*
|
|
||||||
- Тип: *Replace + Hint*
|
|
||||||
- Запрос: *Хочу найти в тексте все неправильные даты в промежутке между 1900 и 2000 годом, а потом исправить их. Если дата правильная - игнорируй ее. Хочу видеть пояснение со ссылками на источники.*
|
|
||||||
|
|
||||||
### Запуск ассистента
|
|
||||||
|
|
||||||
После запуска ассистента пользовательский запрос модифицируется, к нему добавляются правила, чтобы при отправке запроса в ИИ ответ был в нужном для нас формате. Например для того чтобы заменить в тексте все неправильные даты подкрепив их пояснительным текстом, нам необходимо знать какой фрагмент текста был выбран для этой цели, а также сам текст пояснения.
|
|
||||||
|
|
||||||
```js
|
|
||||||
let prompt = `You are a multi-disciplinary text analysis assistant.
|
|
||||||
Your task is to find text fragments that match the user's criteria.`;
|
|
||||||
// ...
|
|
||||||
prompt += `Response format - return ONLY this JSON array with no additional text:
|
|
||||||
[{
|
|
||||||
"origin": "exact text fragment that matches the query",
|
|
||||||
"suggestion": "suggested replacement (plain text)",
|
|
||||||
"reason": "detailed explanation why it matches the criteria",
|
|
||||||
"difference":"visual representation showing exact changes between origin and suggestion"
|
|
||||||
"occurrence": 1,
|
|
||||||
"confidence": 0.95
|
|
||||||
}]
|
|
||||||
\n\n`;
|
|
||||||
prompt += "USER REQUEST:\n```" + assistant.query + "\n```\n\n"; // пользовательский запрос
|
|
||||||
prompt += "TEXT TO ANALYZE:\n```\n" + paragraph_text + "\n```\n\n";
|
|
||||||
// ....
|
|
||||||
```
|
|
||||||
|
|
||||||
Для анализа можно использовать весь текст документа(все параграфы), либо только выделенный фрагмент(только выделенные параграфы). Мы будем рассматривать случай когда обрабатывается только выделенный фрагмент.
|
|
||||||
|
|
||||||
Параграфы с текстом будем получать подписавшись на событие [onParagraphText](https://api.onlyoffice.com/docs/plugin-and-macros/interacting-with-editors/text-document-api/Events/onParagraphText/).
|
|
||||||
|
|
||||||
```js
|
|
||||||
window.Asc.plugin.attachEditorEvent("onParagraphText", (data) => {
|
|
||||||
const {paragraphId, recalcId, text, annotations} = data;
|
|
||||||
console.log("Paragraph updated:", paragraphId);
|
|
||||||
annotations.forEach(a => {
|
|
||||||
console.log(`Annotation ${a.id}: ${a.name} at ${a.start} (${a.length} chars)`);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
Из примера выше у нас есть доступ ко всем параграфам, из них нам нужны только выделенные - достаточно узнать их
|
|
||||||
```id```. Сделать это можно вызвав метод [GetAllParagraphs](https://api.onlyoffice.com/docs/office-api/usage-api/text-document-api/ApiRange/Methods/GetAllParagraphs/) и [GetInternalId](https://api.onlyoffice.com/docs/office-api/usage-api/text-document-api/ApiParagraph/Methods/GetInternalId/):
|
|
||||||
|
|
||||||
```js
|
|
||||||
const range = Api.GetDocument().GetRangeBySelect();
|
|
||||||
const paragraphs = range.GetAllParagraphs();
|
|
||||||
const ids = paragraphs.map(p => p.GetInternalId());
|
|
||||||
```
|
|
||||||
|
|
||||||
Ответ возвращается в таком виде:
|
|
||||||
|
|
||||||
```js
|
|
||||||
let aiAnswer = {
|
|
||||||
origin: "фрагмент соответствующий запросу",
|
|
||||||
suggestion: "предполагаемая замена",
|
|
||||||
reason: "подробоне объяснение почему фрагмент удовлетворяет запросу",
|
|
||||||
difference: "разница между исходным текстом и предполагаемой заменой (в html формате для наглядности)"
|
|
||||||
// --//--
|
|
||||||
occurrence: "Сколько раз вхождение встречается в параграфе (1 раз, 2 раза и т. д.)"
|
|
||||||
confidence: "значение от 0 до 1, процент уверенности в правильном выборе"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Далее отправляем запрос в AI и получвем ответ с нужными нам вхождениями, т.е. с подробной информацией о каждом из них. Результат нужно **отобразить в тексте документа**. Для этого у нас есть метод *[AnnotateParagraph](https://api.onlyoffice.com/docs/plugin-and-macros/interacting-with-editors/text-document-api/Methods/AnnotateParagraph/)* (появился в версии редактора 9.2.0).
|
|
||||||
|
|
||||||
```js
|
|
||||||
window.Asc.plugin.executeMethod("AnnotateParagraph", [{
|
|
||||||
type: "highlightText", // пока что возможно только это значение
|
|
||||||
name: "customAssistant_" + assistantId, // id ассистента
|
|
||||||
paragraphId: "p1", // значение берется из информации о параграфе
|
|
||||||
recalcId: "r12", // значение берется из информации о параграфе
|
|
||||||
ranges: [ // то что нам нужно вычислить опираясь на aiAnswer.origin и aiAnswer.occurance
|
|
||||||
{ start: 5, length: 10, id: "a1" }
|
|
||||||
// start - это порядковый номер первого символа вхождения в параграфе
|
|
||||||
]
|
|
||||||
}]);
|
|
||||||
```
|
|
||||||
|
|
||||||
Аннотации мы в текст мы умеем добавлять, нужно теперь добавить полезных действий. По клику на аннотацию будем показывать всплывающее окно в котором покажем исходный текст, предполагаемую замену и краткое пояснение. Снизу окна будут кнопки ***Принять/Отклонить***. Для этого у нас есть три события:
|
|
||||||
|
|
||||||
1) [```onBlurAnnotation```](https://api.onlyoffice.com/docs/plugin-and-macros/interacting-with-editors/text-document-api/Events/onBlurAnnotation/) - The function called when an annotation loses focus.
|
|
||||||
2) [```onClickAnnotation```](https://api.onlyoffice.com/docs/plugin-and-macros/interacting-with-editors/text-document-api/Events/onClickAnnotation/) - The function called when the user clicks an annotation.
|
|
||||||
3) [```onFocusAnnotation```](https://api.onlyoffice.com/docs/plugin-and-macros/interacting-with-editors/text-document-api/Events/onFocusAnnotation/) - The function called when an annotation receives focus.
|
|
||||||
Все три события возвращают ```{name, paragraphId, rangeId}```
|
|
||||||
|
|
||||||
Из трех событий нам хватит и двух:
|
|
||||||
```onClickAnnotation``` - для того чтобы показыть всплыващее окно.
|
|
||||||
```onBlurAnnotation``` - для того чтобы скрыть всплывающее окно
|
|
||||||
|
|
||||||
|
|
||||||
```js
|
|
||||||
let popup = new window.Asc.PluginWindow();
|
|
||||||
|
|
||||||
let variation = {
|
|
||||||
url : 'annotationPopup.html',
|
|
||||||
isVisual : true,
|
|
||||||
buttons : [{ text:'Accept', primary: true }, { text:'Reject', primary: false }],
|
|
||||||
isModal : false,
|
|
||||||
description: 'Proposal for replacement',
|
|
||||||
EditorsSupport : ["word"],
|
|
||||||
size : [300, 200],
|
|
||||||
fixedSize : true,
|
|
||||||
isTargeted : true // указываем что окно должно появиться рядом с аннотацией
|
|
||||||
};
|
|
||||||
|
|
||||||
window.Asc.plugin.attachEditorEvent("onClickAnnotation", (annotation) => {
|
|
||||||
// --//--
|
|
||||||
popup.show(variation);
|
|
||||||
});
|
|
||||||
|
|
||||||
window.Asc.plugin.attachEditorEvent.attachEditorEvent("onBlurAnnotation", (annotation) => {
|
|
||||||
// --//--
|
|
||||||
popup.close();
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
Для полного счастья не хватает возможности заменять "неправильный" текст на тот что предложил ИИ, либо отказаться от замены.
|
|
||||||
|
|
||||||
```js
|
|
||||||
/**
|
|
||||||
* @param {string} paragraphId
|
|
||||||
* @param {string} rangeId
|
|
||||||
* @param {string} suggestion - текст который ИИ предложил в качестве замены
|
|
||||||
*/
|
|
||||||
async function onAccept(paragraphId, rangeId, suggestion) {
|
|
||||||
|
|
||||||
// выделяем в документе текст с аннотацией, т.е. текст который хотим изменить
|
|
||||||
await new Promise(resolve =>
|
|
||||||
window.Asc.plugin.executeMethod(
|
|
||||||
"SelectAnnotationRange",
|
|
||||||
[{
|
|
||||||
paragraphId: paragraphId,
|
|
||||||
rangeId: rangeId,
|
|
||||||
name: "customAssistant_" + id, // id ассистента
|
|
||||||
}],
|
|
||||||
resolve
|
|
||||||
);
|
|
||||||
);
|
|
||||||
|
|
||||||
// заменяем текст выделенного фрагмента и снимаем выделение
|
|
||||||
Asc.scope.suggestion = aiAnswer.suggestion;
|
|
||||||
await new Promise(resolve => {
|
|
||||||
Asc.plugin.callCommand(
|
|
||||||
() => {
|
|
||||||
Api.ReplaceTextSmart([Asc.scope.suggestion]);
|
|
||||||
Api.GetDocument().RemoveSelection();
|
|
||||||
}
|
|
||||||
false, // нужно ли закрыть окно сразу после выполнения
|
|
||||||
true, // Defines whether the document will be recalculated or not
|
|
||||||
resolve
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
// удаляем аннотацию, т.к. она выполнила свою функцию
|
|
||||||
await new Promise(resolve =>
|
|
||||||
window.Asc.plugin.executeMethod(
|
|
||||||
"RemoveAnnotationRange",
|
|
||||||
[{
|
|
||||||
paragraphId: paragraphId,
|
|
||||||
rangeId: rangeId,
|
|
||||||
name: "customAssistant_" + id, // id ассистента
|
|
||||||
}],
|
|
||||||
resolve
|
|
||||||
);
|
|
||||||
);
|
|
||||||
|
|
||||||
// Returns focus to the editor.
|
|
||||||
await new Promise(resolve =>
|
|
||||||
window.Asc.plugin.executeMethod(
|
|
||||||
"FocusEditor",
|
|
||||||
[],
|
|
||||||
resolve
|
|
||||||
);
|
|
||||||
);
|
|
||||||
},
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
```js
|
|
||||||
/**
|
|
||||||
* @param {string} paragraphId
|
|
||||||
* @param {string} rangeId
|
|
||||||
*/
|
|
||||||
async function onReject(paragraphId, rangeId)
|
|
||||||
{
|
|
||||||
// удаляем аннотацию
|
|
||||||
await new Promise(resolve =>
|
|
||||||
window.Asc.plugin.executeMethod(
|
|
||||||
"RemoveAnnotationRange",
|
|
||||||
[{
|
|
||||||
paragraphId: paragraphId,
|
|
||||||
rangeId: rangeId,
|
|
||||||
name: "customAssistant_" + id, // id ассистента
|
|
||||||
}],
|
|
||||||
resolve
|
|
||||||
);
|
|
||||||
);
|
|
||||||
};
|
|
||||||
```
|
|
||||||
Reference in New Issue
Block a user