HTML-теги с искусственным интеллектом
- среда, 18 сентября 2024 г. в 00:00:04
Всем привет! Помните череду недавних анонсов от IT-гигантов, о различном ИИ-функционале, внедренном во все, что только можно? Среди этих анонсов, например, были помощники в написании текстов писем и сообщений для почтовых и других сервисов. Эти помощники могут проверить вас на ошибки, перевести текст на другой язык, поменять тональность и настроение текста, сделать его более кратким, либо, напротив, дополнить.
На мой скромный взгляд, сейчас складывается такая ситуация, что создавать собственные ИИ-сервисы общего назначения очень рискованно, ибо крупным компаниям не составит труда воспроизвести ваш функционал, имея доступ к более значительным ресурсам, как вычислительным, так и в плане данных. Но и у нас, независимых разработчиков и энтузиастов, есть свои преимущества: мы можем создавать решения на уровне библиотек и компонентов, которые будут способны размыть и скорректировать разницу в конкурентных преимуществах, между гигантами и мелкими стартапами.
Итак, в этом материале, я предлагаю вам вместе со мной создать умный HTML-тег - текстовое поле, которое сможет помогать пользователю в настройке введенного текста. Этот тег можно будет использовать на любом сайте, в любом веб-приложении, созданном с помощью любого современного фреймворка, или даже, в простом статическом HTML-файле.
Важно: мы создадим упрощенный концептуальный пример для демонстрации основных принципов, который можно будет настроить под ваши нужды впоследствии: учлучшить UX, проработать дизайн, реализовать доступ к API, согласно конкретным политикам взаимодействия с пользователями.
Для начала, определимся с технологиями. Для приготовления блюда по нашему рецепту, нам необходимы 2 ключевых ингредиента: библиотека для удобной работы с кастомными HTML-тегами и API для доступа к ИИ-модели.
Для работы с тегами я выбираю Symbiote.js, библиотеку, идеально подходящую для создания компонентов-агностиков и универсальных виджетов для веб.
Иатк, сначала устанавим Симбиот в наш проект:
npm i @symbiotejs/symbiote
Затем, создаем JavaScript файл для нашего веб-компонента c каркасом для нашего будущего кода (smart-textarea.js):
import Symbiote, { html, css } from '@symbiotejs/symbiote';
export class SmartTextarea extends Symbiote {
// Объект, инициализирующий состояние и основные сущности компонента:
init$ = {}
}
// Тут будут стили компонента:
SmartTextarea.rootStyles = css``;
// Тут будет шаблон:
SmartTextarea.template = html``;
// Регистрируем кастомный тег в реестре браузера:
SmartTextarea.reg('smart-textarea');
Если вы знакомы с любым современным фронтенд-фреймворком или библиотекой, думаю, вам будет, в целом, очень знакомо и понятно все, что вы тут видите. Дополнительно рассказать стоит только о интерфейсе rootStyles
, который позволяет задавать стили независимо от того, находится ли ваш компонент в каком-либо контексте Shadow DOM верхнего уровня или нет. Стили будут добавлены именно в тот root
, того сегмента DOM-дерева, в котором используется наш компонент, что позволяет гибко управлять отображением с минимальной зависимостью от того, что наш компонент окружает, с возможностью изоляции всего, что вы посчитаете необходимым изолировать.
Теперь, давайте создадим HTML-файл, использующий наш умный тег:
<script type="importmap">
{
"imports": {
"@symbiotejs/symbiote": "https://esm.run/@symbiotejs/symbiote"
}
}
</script>
<script type="module" src="./smart-textarea.js"></script>
<smart-textarea model="gpt-4o-mini"></smart-textarea>
В данном примере вы видите только то, что представляет важность для отображения и тестирования поведения нашего текстового поля. Этого достаточно для продолжения работы, мы просто оставим за скобками все остальные элементы обычного веб-документа, типа head
, body
и т. д.
Важным тут является блок c importmap
. В нашем примере, мы подключим библиотеку Symbiote.js через CDN, что позволит нам, впоследствии, эффективно и многократно использовать общую зависимость между разными независимыми компонентами приложения, без необходимости использовать какие-то отдельные громоздкие решения для этого (типа Module Federation). При этом, поскольку мы, изначально, установили зависимость через npm, нам будет доступно всё необходимое, для работы инструментов окружения разработки: декларации типов для поддержки TypeScript, переход к определениям сущностей и так далее.
Приступаем, непосредственно, к работе над функционалом.
Создаем рабочий шаблон:
SmartTextarea.template = html`
<textarea
${{oninput: 'saveSourceText'}}
placeholder="AI assisted text input..."
ref="text"></textarea>
<input
type="text"
placeholder="Preferred Language"
ref="lang">
<label>Text style: {{+currentTextStyle}}</label>
<input
${{onchange: 'onTextStyleChange'}}
type="range"
min="1"
max="${textStyles.length}"
step="1"
ref="textStyleRange">
<button ${{onclick: 'askAi'}}>Rewrite text</button>
<button ${{onclick: 'revertChanges'}}>Revert AI changes</button>
`;
Для лучшей подсветки синтаксиса шаблонных литералов в JS, вы можете установить одно из множества расширений для своей IDE, но тут на Хабре - мы имеем что имеем. Сейчас я объясню все базовые моменты и станет понятнее, как работает шаблон.
Первая конструкция, которую мы встречаем - это привязка обработчика к элементу:
`${{oninput: 'saveSourceText'}}`
В ней мы видим обычный синтаксис шаблонного литерала с объектом, описывающим привязку логики компонента к DOM-элементам шаблона. Ключами, в таком объекте, являются собственные свойства элементов, а значения - текстовыми ключами к сущностям состояния компонента-симбиота.
Вторая конструкция это:
{{+currentTextStyle}}
Таким образом, (двойные фигурные скобки, без символа $
) в Symbiote.js осуществляется привязка данных к текстовым нодам. Плюс +
в начале имени, говорит о том, что свойство является вычислимым, то есть, оно получается автоматически при изменении свойств состояния или при принудительно, с помощью специального метода notify
.
И, наконец:
`${textStyles.length}`
это самая банальная интерполяция строк, где в шаблон прямо подставляется значение без какой-либо дополнительной магии.
Итого, мы имеем шаблон компонента, который содержит:
основное текстовое поле
поле для ввода нужно языка в свободном формате (например, можно написать "аргентинский испанский", или "старославянский")
отображение выбранного стиля текста
кнопку генерации текста
кнопку возврата к исходному тексту пользователя
Теперь приступим к описанию свойств и методов, которые мы привязываем к шаблону.
export class SmartTextarea extends Symbiote {
// Храним исходный текст пользователя в приватном свойстве класса:
#sourceText = '';
init$ = {
// Имя LLM по умолчанию:
'@model': 'gpt-4o',
// Вычислимое свойство (computed property),
// содержит описание стиля, к которому нужно привести наш текст:
'+currentTextStyle': () => {
return textStyles[this.ref.textStyleRange.value - 1];
},
// Сохраняем текст пользователя, для функции отмены изменений:
saveSourceText: () => {
this.#sourceText = this.ref.text.value;
},
// Возвращаем текстовое поле к исходному тексту:
revertChanges: () => {
this.ref.text.value = this.#sourceText;
},
// Реагируем на выбор стиля текста:
onTextStyleChange: (e) => {
// Принудительно вызываем расчет вычислимого свойства:
this.notify('+currentTextStyle');
},
// ...
}
}
Содержимое 8-й строки вышеприведенного кода, имеет следующее объяснение: свойства состояния компонента, имена которых начинаются символом @
, автоматически привязываются к значению HTML-атрибутов нашего кастомного тега, если те будут явно заданы. Если атрибут не задан, свойство будет иметь значение по умолчанию, полученное при инициализации, в нашем случае - gpt-4o
.
На 12-й строке мы видим вычисляемое свойство, с префиксом +
, значение которого будет получено в результате выполнения функции.
Методы saveSourceText
и revertChanges
, думаю, не нуждаются в дополнительных объяснениях, это просто обработчики нажатий на кнопки в шаблоне.
Метод onTextStyleChange
- это обработчик изменений положения слайдера, который принудительно вызывает расчет значение свойства +currentTextStyle
. Для работы этого метода и вычисления текущего значения, нам нужен массив с описаниями стилей текста, который мы создадим в отдельном модуле textStyles.js
, со следующим содержимым:
export const textStyles = [
'Free informal speech, jokes, memes, emoji, possibly long',
'Casual chat, friendly tone, occasional emoji, short and relaxed',
'Medium formality, soft style, basic set of emoji possible, compact',
'Neutral tone, clear and direct, minimal slang or emoji',
'Professional tone, polite and respectful, no emoji, short sentences',
'Strict business language. Polite and grammatically correct.',
'Highly formal, authoritative, extensive use of complex vocabulary, long and structured',
];
Написать описания стилей текста, с ранжированием от самого неформального, до самого строгого, мы, конечно же, попросили сам ChatGPT.
Также, в приведенном выше коде, мы видим примеры обращений к элементам, описанным в шаблоне, через интерфейс ref
, например:
this.ref.text.value
Это чем-то похоже на то, как это работает в React и нужно для того, чтобы не искать элементы вручную через DOM API. По сути, this.ref
- это коллекция ссылок на DOM-элементы, для которых задан соответствующий атрибут в HTML-шаблоне, например: ref="text"
Сейчас нам требуется сделать самое главное: попросить ИИ переписать наш текст в согласии с полученными настройками. В этом примере, я сделаю это максимально простым способом, без использования дополнительных библиотек и каких-либо слоев для контроля доступа, отправив прямой запрос к API:
// ...
export class SmartTextarea extends Symbiote {
// ...
init$ = {
// ...
askAi: async () => {
// Если текстовое поле пустое, отменяем все и выводим алерт:
if (!this.ref.text.value.trim()) {
alert('Your text input is empty');
return;
}
// Отправляем запрос к API эндпоинту, взятому из конфигурации:
let aiResponse = await (await window.fetch(CFG.apiUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
// Берем ключ для работы с API из скрытого от git js-модуля:
Authorization: `Bearer ${CFG.apiKey}`,
},
body: JSON.stringify({
// Читаем название нужной модели из HTML-атрибута (gpt-4o-mini),
// либо используем модель по умолчанию (gpt-4o):
model: this.$['@model'],
messages: [
{
role: 'system',
// Передаем в модель настройки языка и тона:
content: JSON.stringify({
useLanguage: this.ref.lang.value || 'Same as the initial text language',
textStyle: this.$['+currentTextStyle'],
}),
},
{
role: 'assistant',
// Описываем роль ИИ-ассистента:
content: 'You are the text writing assistant. Rewrite the input text according to parameters provided.',
},
{
role: 'user',
// Передаем сам текст, который ходим модифицировать:
content: this.ref.text.value,
},
],
temperature: 0.7,
}),
})).json();
// Дожидаемся ответа и обновляем текст в поле ввода:
this.ref.text.value = aiResponse?.choices?.[0]?.message.content || this.ref.text.value;
},
}
}
В этом блоке кода, интересным является способ доступа к значениям стейта нашего компонента Symbiote.js. Выглядит он так:
this.$['@model']
// Или:
this.$['+currentTextStyle']
// Или просто:
this.$.myProperty // для обычных свойств без префиксов
Теперь, нам нужно создать модуль конфигураций (secret.js), который мы спрячем от чужих глаз через .gitignore
:
export const CFG = {
apiUrl: 'https://api.openai.com/v1/chat/completions',
apiKey: '<YOUR_API_KEY>',
};
Внимание! Конечно же, этот пример нельзя использовать в проде, или каком-либо ином неконтролируемом публичном виде. В реальных условиях, вам нужно будет дополнительно защитить ваши ключи, либо дать клиенту возможность использовать свои настройки доступа.
Для данного примера, я использовал API от OpenAI, но вы можете использовать любой другой подходящий ИИ-сервис, self-hosted модель или свой middleware.
Нам осталось добавить стили нашему веб-компоненту. Я не стану посвящать этому много внимания, так как это не очень важно в нашем случае:
// ...
SmartTextarea.rootStyles = css`
smart-textarea {
display: inline-flex;
flex-flow: column;
gap: 10px;
width: 500px;
textarea {
width: 100%;
height: 200px;
}
}
`;
// ...
Приведу полный код получившегося компонента:
import Symbiote, { html, css } from '@symbiotejs/symbiote';
import { CFG } from './secret.js';
import { textStyles } from './textStyles.js';
export class SmartTextarea extends Symbiote {
#sourceText = '';
init$ = {
'@model': 'gpt-4o',
'+currentTextStyle': () => {
return textStyles[this.ref.textStyleRange.value - 1];
},
saveSourceText: () => {
this.#sourceText = this.ref.text.value;
},
revertChanges: () => {
this.ref.text.value = this.#sourceText;
},
onTextStyleChange: (e) => {
this.notify('+currentTextStyle');
},
askAi: async () => {
if (!this.ref.text.value.trim()) {
alert('Your text input is empty');
return;
}
let aiResponse = await (await window.fetch(CFG.apiUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${CFG.apiKey}`,
},
body: JSON.stringify({
model: this.$['@model'],
messages: [
{
role: 'system',
content: JSON.stringify({
useLanguage: this.ref.lang.value || 'Same as the initial text language',
textStyle: this.$['+currentTextStyle'],
}),
},
{
role: 'assistant',
content: 'You are the text writing assistant. Rewrite the input text according to parameters provided.',
},
{
role: 'user',
content: this.ref.text.value,
},
],
temperature: 0.7,
}),
})).json();
this.ref.text.value = aiResponse?.choices?.[0]?.message.content || this.ref.text.value;
},
}
}
SmartTextarea.rootStyles = css`
smart-textarea {
display: inline-flex;
flex-flow: column;
gap: 10px;
width: 500px;
textarea {
width: 100%;
height: 200px;
}
}
`;
SmartTextarea.template = html`
<textarea
${{oninput: 'saveSourceText'}}
placeholder="AI assisted text input..."
ref="text"></textarea>
<input
type="text"
placeholder="Preferred Language"
ref="lang">
<label>Text style: {{+currentTextStyle}}</label>
<input
${{onchange: 'onTextStyleChange'}}
type="range"
min="1"
max="${textStyles.length}"
step="1"
ref="textStyleRange">
<button ${{onclick: 'askAi'}}>Rewrite text</button>
<button ${{onclick: 'revertChanges'}}>Revert AI changes</button>
`;
SmartTextarea.reg('smart-textarea');
Открыв наш HTML-файл в браузере, мы увидим следующее:
Готово. Теперь мы можем использовать тег <smart-textarea></smart-textarea>
в шаблонах других компонентов, написанных с использованием любых других современных фреймворков; в разметке, которая генерируется на сервере с помощью любого шаблонизатора или генератора статики, в простых HTML-файлах с формами, и так далее.
В этом примере, я не коснулся вопроса сборки и настройки деплоя для подобных компонентов-агностиков. Также, я не углублялся в вопрос стилизации. Если аудитория проявит интерес к данной теме - напишу отдельную статью. Но если кратко, то для сборки вы можете использовать любой современный сборщик. Я, к примеру, чаще всего использую esbiuld.
Symbiote.js поддерживает синтаксис шаблонов, полностью основанный на HTML-тегах и их атрибутах, поэтому, вы можете выносить и описывать шаблоны как отдельные HTML-файлы и использовать соответствующий лоадер для своего сборщика. Также, вы можете внедрять такие шаблоны как часть общего HTML-файла и брать их под контроль уже постфактум, с помощью тега-враппера, который сам инициализирует вашу логику. Это именно то, как должны работать настоящие серверные компоненты, что выгодно отличает Symbiote.js от множества, сходных по назначению, решений для более популярных библиотек. Все, что вам нужно для этого сделать - это использовать флаг ssrMode
Этот материал является логическим продолжением моей предыдущей статьи, где я рассматриваю веб-технологии в роли важного стабилизирующего фактора, в практической работе с ИИ.