Create Your Own HTML5 Tag With ChatGPT Autocomplete, Text Replacement, or Translation
Add to your RSS feed19 September 20248 min readTable of Contents
Among the modern HTML standards and specifications, there’s something called Custom Elements. For those unfamiliar, it's a way to create your own tags, which the browser automatically initializes when it encounters them in the markup, executing the specific behavior logic you’ve defined. Additionally, there’s a way to modify the behavior of standard tags (though the nuances of this are beyond the scope of this discussion).
In this tutorial, we will create a smart HTML tag—a text field that helps users format the text they enter. This tag can be used on any website, in any web application built with modern frameworks, or even in a simple static HTML file.
Preparatory Steps
First, let's define the technologies we'll be using. We need to initialize a project with Parcel, get familiar with the Symbiote.js library, and obtain an API key from ChatGPT. If you are not familiar with Custom Elements, please read the howto.
Install Symbiote.js
SymbioteJS is a lightweight JavaScript library designed to simplify the creation of web components and improve the development experience with custom HTML elements. Built with modern web standards in mind, it offers a simple and efficient way to structure, style, and manage reusable components, without the need for heavy frameworks.
Create a Component
Next, we’ll create our component. Since this project is quite small, we’ll implement it directly in the app.ts file.
1 import Symbiote, { html, css } from '@symbiotejs/symbiote';23 export class SmartTextarea extends Symbiote {4 // The object that initializes the state and core entities of the component:5 init$ = {};6 }78 // Styles of component9 SmartTextarea.rootStyles = css``;1011 // Template of component:12 SmartTextarea.template = html``;1314 // Define a custom HTML tag:15 SmartTextarea.reg('smart-textarea');
Now let's create a HTML file
1 <script type="importmap">2 {3 "imports": {4 "@symbiotejs/symbiote": "https://esm.run/@symbiotejs/symbiote"5 }6 }7 </script>8 <script type="module" src="./smart-textarea.js"></script>910 <smart-textarea model="gpt-4o-mini"></smart-textarea>
An important aspect here is the block with the import map. In our example, we will include the SymbioteJS library via CDN, which will allow us to efficiently and repeatedly share a common dependency among different independent components of the application, without the need for bulky solutions like Module Federation. Additionally, since we initially installed the dependency through NPM, we will have access to everything necessary for our development environment tools, including type declarations for TypeScript support, entity definitions, and more.
Template
Let's create a template
1 SmartTextarea.template = html`2 <textarea3 ${{ oninput: 'saveSourceText' }}4 placeholder="AI assisted text input..."5 ref="text"6 ></textarea>78 <input type="text" placeholder="Preferred Language" ref="lang" />910 <label>Text style: {{+currentTextStyle}}</label>11 <input12 ${{ onchange: 'onTextStyleChange' }}13 type="range"14 min="1"15 max="${textStyles.length}"16 step="1"17 ref="textStyleRange"18 />1920 <button ${{ onclick: 'askAi' }}>Rewrite text</button>21 <button ${{ onclick: 'revertChanges' }}>Revert AI changes</button>22 `;
The code snippet defines a template for a SmartTextarea component using a template literal. This template describes the HTML structure of the component, along with some dynamic bindings and event handlers.
1 {2 {3 +currentTextStyle;4 }5 }
The plus sign (+) at the beginning of the name indicates that the property is computed, meaning it is automatically derived when the state properties change or can be manually triggered using a special method called notify.
State Entities and Handlers
Now let's describe the properties and methods that we bind to the template.
1 export class SmartTextarea extends Symbiote {2 // Store the user's original text in a private class property3 #sourceText = '';45 init$ = {6 // LLM name by default7 '@model': 'gpt-4o',89 // The computed property contains the description of the style to which we need to format our text.10 '+currentTextStyle': () => {11 return textStyles[this.ref.textStyleRange.value - 1];12 },1314 // Save the user's text for the undo function.15 saveSourceText: () => {16 this.#sourceText = this.ref.text.value;17 },18 // Restore the textarea to the original text.19 revertChanges: () => {20 this.ref.text.value = this.#sourceText;21 },22 // Respond to text style selection.23 onTextStyleChange: (e) => {24 // Manually trigger the recalculation of the computed property.25 this.notify('+currentTextStyle');26 },2728 // ...29 };30 }
Now we need an array containing descriptions of text styles, which we will create in a separate module called textStyles.ts with the following content:
1 export const textStyles: string[] = [2 'Free informal speech, jokes, memes, emoji, possibly long',3 'Casual chat, friendly tone, occasional emoji, short and relaxed',4 'Medium formality, soft style, basic set of emoji possible, compact',5 'Neutral tone, clear and direct, minimal slang or emoji',6 'Professional tone, polite and respectful, no emoji, short sentences',7 'Strict business language. Polite and grammatically correct.',8 'Highly formal, authoritative, extensive use of complex vocabulary, long and structured',9 ];
Additionally, in the code above, we can see examples of how to access the elements described in the template using the ref interface, such as:
1 this.ref.text.value;
This is similar to how it works in React and helps avoid manually searching for elements using the DOM API. Essentially, this.ref is a collection of references to DOM elements that have the corresponding attribute set in the HTML template, such as ref="text".
Request to the LLM
Now we need to do the most important thing: ask the AI to rewrite our text according to the specified settings. In this example, I will keep it as simple as possible, without using any additional libraries or access control layers, by sending a direct request to the API:
1 export class SmartTextarea extends Symbiote {2 // ...34 init$ = {5 // ...67 askAi: async () => {8 // If the textarea is empty, we cancel everything and display an alert:9 if (!this.ref.text.value.trim()) {10 alert('Your text input is empty');11 return;12 }1314 // We send a request to the API endpoint taken from the configuration:15 let aiResponse = await (16 await window.fetch(CFG.apiUrl, {17 method: 'POST',18 headers: {19 'Content-Type': 'application/json',2021 // We retrieve the API key from a hidden JavaScript module that is not tracked by git:22 Authorization: `Bearer ${CFG.apiKey}`,23 },24 body: JSON.stringify({25 // Read the name of the required model from the HTML attribute (gpt-4o-mini),26 // or use the default model (gpt-4o):27 model: this.$['@model'],28 messages: [29 {30 role: 'system',3132 // Pass the language and tone settings to the model:33 content: JSON.stringify({34 useLanguage: this.ref.lang.value || 'Same as the initial text language',35 textStyle: this.$['+currentTextStyle'],36 }),37 },38 {39 role: 'assistant',4041 // Describe the role of the AI assistant:42 content:43 'You are the text writing assistant. Rewrite the input text according to parameters provided.',44 },45 {46 role: 'user',4748 // Pass the text that we want to modify:49 content: this.ref.text.value,50 },51 ],52 temperature: 0.7,53 }),54 })55 ).json();5657 // Wait for the response and update the text in the input field:58 this.ref.text.value = aiResponse?.choices?.[0]?.message.content || this.ref.text.value;59 },60 };61 }
Now, we need to create a configuration module (secret.ts), which we will hide from prying eyes using .gitignore:
1 export const CFG = {2 apiUrl: 'https://api.openai.com/v1/chat/completions',3 apiKey: '<YOUR_API_KEY>',4 };
Styles
We just need to add styles to our web component.
1 // ...23 SmartTextarea.rootStyles = css`4 smart-textarea {5 display: inline-flex;6 flex-flow: column;7 gap: 10px;8 width: 500px;910 textarea {11 width: 100%;12 height: 200px;13 }14 }15 `;1617 // ...
Full code
1 import Symbiote, { html, css } from '@symbiotejs/symbiote';2 import { CFG } from './secret.js';3 import { textStyles } from './textStyles.js';45 export class SmartTextarea extends Symbiote {6 #sourceText = '';78 init$ = {9 '@model': 'gpt-4o',1011 '+currentTextStyle': () => {12 return textStyles[this.ref.textStyleRange.value - 1];13 },1415 saveSourceText: () => {16 this.#sourceText = this.ref.text.value;17 },18 revertChanges: () => {19 this.ref.text.value = this.#sourceText;20 },21 onTextStyleChange: (e) => {22 this.notify('+currentTextStyle');23 },24 askAi: async () => {25 if (!this.ref.text.value.trim()) {26 alert('Your text input is empty');27 return;28 }29 let aiResponse = await (30 await window.fetch(CFG.apiUrl, {31 method: 'POST',32 headers: {33 'Content-Type': 'application/json',34 Authorization: `Bearer ${CFG.apiKey}`,35 },36 body: JSON.stringify({37 model: this.$['@model'],38 messages: [39 {40 role: 'system',41 content: JSON.stringify({42 useLanguage: this.ref.lang.value || 'Same as the initial text language',43 textStyle: this.$['+currentTextStyle'],44 }),45 },46 {47 role: 'assistant',48 content:49 'You are the text writing assistant. Rewrite the input text according to parameters provided.',50 },51 {52 role: 'user',53 content: this.ref.text.value,54 },55 ],56 temperature: 0.7,57 }),58 })59 ).json();6061 this.ref.text.value = aiResponse?.choices?.[0]?.message.content || this.ref.text.value;62 },63 };64 }6566 SmartTextarea.rootStyles = css`67 smart-textarea {68 display: inline-flex;69 flex-flow: column;70 gap: 10px;71 width: 500px;7273 textarea {74 width: 100%;75 height: 200px;76 }77 }78 `;7980 SmartTextarea.template = html`81 <textarea82 ${{ oninput: 'saveSourceText' }}83 placeholder="AI assisted text input..."84 ref="text"85 ></textarea>8687 <input type="text" placeholder="Preferred Language" ref="lang" />8889 <label>Text style: {{+currentTextStyle}}</label>90 <input91 ${{ onchange: 'onTextStyleChange' }}92 type="range"93 min="1"94 max="${textStyles.length}"95 step="1"96 ref="textStyleRange"97 />9899 <button ${{ onclick: 'askAi' }}>Rewrite text</button>100 <button ${{ onclick: 'revertChanges' }}>Revert AI changes</button>101 `;102103 SmartTextarea.reg('smart-textarea');
This template sets up a user interface that allows users to input text, specify a preferred language, adjust the style of the text using a range input, and interact with an AI for rewriting text. The use of ref and event bindings provides a way to interact with these elements programmatically within the SmartTextarea component, making it dynamic and responsive to user actions.
Now, run:
Now we can use the