Как заставить ИИ на базе LLM писать полноценные приложения на HTML + CSS + JavaScript

от автора

Зачем вообще это делать?

Во первых это настоящий вызов современным LLM моделям и очень интересно как разные модели будут справляться с такой задачей.

Во вторых у такого решения есть миллион способов применения во всех сферах от игровых приложений, MiniApp до бизнес задач, как например написать калькулятор стоимости на сайт.

Ну раз надо так надо, делаем )

Казалось бы что может быть проще? Берем любой чат с ИИ и говорим «напиши игру Змейка»? И нет сомнений что вы получите код страницы с рабочей версией игры.

Но.. а если вам нужно изменить в предложенном ИИ коде что-то или добавить функционала? Если вы с такими вопросами продолжите диалог с ИИ то очень быстро увидите что он начинает путаться, переписывать уже работающие блоки кода или еще хуже — выдавать вам куски кода, который нужно заменить, а остальной код не трогать.

Как же сделать так чтобы ИИ можно сам менять часть уже написанного им кода таким образом чтобы не испортить уже работающие части, а так же дописывать новый функционал к странице?

А вот как!

Давайте любой код HTML разобьем на блоки/кирпичики/слоты и заставим ИИ писать код используя такие блоки, а самое главное потом заменять/удалять/добавлять такие блоки САМОСТОЯТЕЛЬНО.

Вот как будет выглядеть тогда пустая страница, разбитая на такие виртуальные блоки:

<html>     <head>         <!-- HEAD_BLOCK_1 --><meta charset="UTF-8"><title>My Page</title><!-- /HEAD_BLOCK_1 -->         <!-- HEAD_BLOCK_2 --><style>body {color:#333;background:#ccc;}</style><!-- /HEAD_BLOCK_2 -->         <!-- HEAD_BLOCK_3 --><script>alert('Hello World!')</script><!-- /HEAD_BLOCK_3 -->     </head>     <body>         <!-- BODY_BLOCK_1 --><header><h1>Welcome</h1></header><!-- /BODY_BLOCK_1 -->         <!-- BODY_BLOCK_2 --><main><p>Main content here</p></main><!-- /BODY_BLOCK_2 -->         <!-- BODY_BLOCK_3 --><footer>Footer content</footer><!-- /BODY_BLOCK_3 -->     </body> </html>

Главный фокус

И сразу разоблачение: мы НИКОГДА не будем показывать ИИ сам код HTML!

Мы подготовим словарь, содержащий блоки и показывать будем только его.

Вот код функции, которая создает словарь в Python по нашей структуре HTML документа:

import re  def html_to_dict(html):     structure = {         'html': {             'head': {'blocks': []},             'body': {'blocks': []}         }     }     head_pattern = r'<!-- HEAD_BLOCK_\d+ -->(.*?)<!-- /HEAD_BLOCK_\d+ -->'     body_pattern = r'<!-- BODY_BLOCK_\d+ -->(.*?)<!-- /BODY_BLOCK_\d+ -->'     head_blocks = re.findall(head_pattern, html, re.DOTALL)     body_blocks = re.findall(body_pattern, html, re.DOTALL)     structure['html']['head']['blocks'] = head_blocks     structure['html']['body']['blocks'] = body_blocks     return structure

В результате наш код пустой странице после выполнения этого кода будет выглядеть так:

{     "html": {         "body": {             "blocks": [                 "<header><h1>Welcome</h1></header>",                 "<main><p>Main content here</p></main>",                 "<footer>Footer content</footer>"             ]         },         "head": {             "blocks": [                 "<meta charset=\"UTF-8\"><title>My Page</title>",                 "<style>body {color:#333;background:#ccc;}</style>",                 "<script>alert('Hello World!')</script>"             ]         }     } }

И вот именно в этом виде ИИ будет работать с кодом.

Теперь немного промптинга, куда уж без него )

Роль для ИИ может выглядеть так:

Твоя задача - создавать и редактировать HTML/CSS/JavaScript код.      При этом код страницы соответствует такому шаблону: ``` <html>     <head>         <!-- HEAD_BLOCK_1 --><meta charset="UTF-8"><title>My Page</title><!-- /HEAD_BLOCK_1 -->         <!-- HEAD_BLOCK_2 --><style>body {color:#333;background:#ccc;}</style><!-- /HEAD_BLOCK_2 -->         <!-- HEAD_BLOCK_3 --><script>alert('Hello World!')</script><!-- /HEAD_BLOCK_3 -->     </head>     <body>         <!-- BODY_BLOCK_1 --><header><h1>Welcome</h1></header><!-- /BODY_BLOCK_1 -->         <!-- BODY_BLOCK_2 --><main><p>Main content here</p></main><!-- /BODY_BLOCK_2 -->         <!-- BODY_BLOCK_3 --><footer>Footer content</footer><!-- /BODY_BLOCK_3 -->     </body> </html> ```  Твой ответ должен быть всегда только в виде строк с описанием изменений: ``` head:::1:::@@@<meta charset="UTF-8"><title>My Website</title>@@@&&& head:::2:::@@@<style>body {color:#000;}</style>@@@&&& head:::3:::@@@<meta name="viewport" content="width=device-width, initial-scale=1.0">@@@&&& head:::4:::@@@<script>alert('Hello');</script>@@@&&& body:::1:::@@@<header><h1>Welcome to our site</h1></header>@@@&&& body:::2:::@@@<nav><ul><li>Home</li><li>About</li><li>Contact</li></ul></nav>@@@&&& body:::3:::@@@<main><article><h2>Latest News</h2><p>Content here</p></article></main>@@@&&& body:::4:::@@@<aside><div class="widget">Sidebar content</div></aside>@@@&&& body:::5:::@@@<footer><p>© 2024 My Company</p></footer>@@@&&& ```  Важно чтобы стили и скрипты были описаны внутри HTML, исключения могут быть только ссылки на библиотеки стилей (например bootstrap) или JavaScript (jquery) должны быть только в виде внешних ссылок. Если ты добавляешь код, которого еще не было, то добавляй его в новый номер блока. Если ты хочешь изменить код в блоке, который уже есть, то обязательно ответь полным кодом этого блока.

Это не идеальный но уже вполне рабочий промпт, который слушается большинство LLM моделей.

Теперь посмотрим как выглядит запрос к ИИ:

# html_dict - словарь, полученный из HTML на предыдущем шаге # task - Задача, например: "Создай игру Змейка"  api_key = 'sk-XXXXXXXX-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX' client = openai.OpenAI(api_key=api_key) response = client.chat.completions.create(     model="gpt-4o-mini", #"o1-mini",     messages=[         {             "role": "user",             "content": role # - это наш промпт, приведенный выше         },         {             "role": "user",             "content": f"Текущий код страницы: ```{html_dict}``` и вот новая задача: ```{task}```"         }     ] ) ai_reply = response.choices[0].message.content

И вот какой ответ мы получим от ИИ при пустой странице и запросе «создай игру Змейка» (на модели gpt-4o-mini):

head:::1:::@@@<meta charset="UTF-8"><title>Snake Game</title>@@@&&& head:::2:::@@@<style>body {color:#000; background:#f0f0f0; display: flex; align-items: center; justify-content: center; height: 100vh; margin: 0; } canvas { border: 1px solid #000; }</style>@@@&&& head:::3:::@@@<script>let canvas, ctx, snake, food, score, direction; function init() { canvas = document.createElement('canvas'); ctx = canvas.getContext('2d'); document.body.appendChild(canvas); canvas.width = 400; canvas.height = 400; snake = [{ x: 10, y: 10 }]; food = { x: Math.floor(Math.random() * 20), y: Math.floor(Math.random() * 20) }; score = 0; direction = { x: 0, y: 0 }; window.addEventListener('keydown', changeDirection); gameLoop(); } function gameLoop() { update(); draw(); setTimeout(gameLoop, 100); } function update() { const head = { x: snake[0].x + direction.x, y: snake[0].y + direction.y }; snake.unshift(head); if (head.x === food.x && head.y === food.y) { score++; food = { x: Math.floor(Math.random() * 20), y: Math.floor(Math.random() * 20) }; } else { snake.pop(); } } function draw() { ctx.clearRect(0, 0, canvas.width, canvas.height); for (let segment of snake) { ctx.fillStyle = 'green'; ctx.fillRect(segment.x * 20, segment.y * 20, 20, 20); } ctx.fillStyle = 'red'; ctx.fillRect(food.x * 20, food.y * 20, 20, 20); ctx.fillStyle = 'black'; ctx.fillText('Score: ' + score, 10, 20); } function changeDirection(event) { if (event.key === 'ArrowUp') { direction = { x: 0, y: -1 }; } else if (event.key === 'ArrowDown') { direction = { x: 0, y: 1 }; } else if (event.key === 'ArrowLeft') { direction = { x: -1, y: 0 }; } else if (event.key === 'ArrowRight') { direction = { x: 1, y: 0 }; } } window.onload = init;</script>@@@&&& body:::1:::@@@<header><h1>Snake Game</h1></header>@@@&&& body:::2:::@@@<main><p>Use arrow keys to move the snake and eat the food!</p></main>@@@&&& body:::3:::@@@<footer><p>© 2024 Snake Game</p></footer>@@@&&&

Назад в HTML!

Теперь наша задача из этой абракадабры обратно собрать HTML код )

Сначала напишем функцию, которая в нашем словаре внесет правки предлложенные ИИ:

import copy  def merge_changes(current_structure, changes):     new_structure = {         'html': {             'head': {'blocks': copy.deepcopy(current_structure['html']['head']['blocks'])},             'body': {'blocks': copy.deepcopy(current_structure['html']['body']['blocks'])}         }     }     for change in changes:         if change and ':::' in change:             section, number, content = change.split(':::', 2)             section = section.replace('\n', '').replace(' ', '')             content = content.split('@@@')[1].replace('\n', '').replace('\r', '')             block_index = int(number) - 1             if section == 'head':                 while len(new_structure['html']['head']['blocks']) <= block_index:                     new_structure['html']['head']['blocks'].append('')                 new_structure['html']['head']['blocks'][block_index] = content             elif section == 'body':                 while len(new_structure['html']['body']['blocks']) <= block_index:                     new_structure['html']['body']['blocks'].append('')                 new_structure['html']['body']['blocks'][block_index] = content     return new_structure      # changes - это ответ от ИИ с предложенным списком изменений   new_structure = merge_changes(current_structure, changes.split('&&&\n'))

Итогом работы этой функции будет обновленный словарь вида:

{     "html": {         "body": {             "blocks": [                 "<header><h1>Snake Game</h1></header>",                 "<main><p>Use arrow keys to move the snake and eat the food!</p></main>",                 "<footer><p>\u00a9 2024 Snake Game</p></footer>"             ]         },         "head": {             "blocks": [                 "<meta charset=\"UTF-8\"><title>Snake Game</title>",                 "<style>body {color:#000; background:#f0f0f0; display: flex; align-items: center; justify-content: center; height: 100vh; margin: 0; } canvas { border: 1px solid #000; }</style>",                 "<script>let canvas, ctx, snake, food, score, direction; function init() { canvas = document.createElement('canvas'); ctx = canvas.getContext('2d'); document.body.appendChild(canvas); canvas.width = 400; canvas.height = 400; snake = [{ x: 10, y: 10 }]; food = { x: Math.floor(Math.random() * 20), y: Math.floor(Math.random() * 20) }; score = 0; direction = { x: 0, y: 0 }; window.addEventListener('keydown', changeDirection); gameLoop(); } function gameLoop() { update(); draw(); setTimeout(gameLoop, 100); } function update() { const head = { x: snake[0].x + direction.x, y: snake[0].y + direction.y }; snake.unshift(head); if (head.x === food.x && head.y === food.y) { score++; food = { x: Math.floor(Math.random() * 20), y: Math.floor(Math.random() * 20) }; } else { snake.pop(); } } function draw() { ctx.clearRect(0, 0, canvas.width, canvas.height); for (let segment of snake) { ctx.fillStyle = 'green'; ctx.fillRect(segment.x * 20, segment.y * 20, 20, 20); } ctx.fillStyle = 'red'; ctx.fillRect(food.x * 20, food.y * 20, 20, 20); ctx.fillStyle = 'black'; ctx.fillText('Score: ' + score, 10, 20); } function changeDirection(event) { if (event.key === 'ArrowUp') { direction = { x: 0, y: -1 }; } else if (event.key === 'ArrowDown') { direction = { x: 0, y: 1 }; } else if (event.key === 'ArrowLeft') { direction = { x: -1, y: 0 }; } else if (event.key === 'ArrowRight') { direction = { x: 1, y: 0 }; } } window.onload = init;</script>"             ]         }     } }

Ну и наконец-то мы теперь можем собрать обратно HTML код вот такой функцией:

def dict_to_html(structure):     html = ['<html>', '<head>']     for i, block in enumerate(structure['html']['head']['blocks'], 1):         if block:  # Проверяем, что блок не пустой             html.append(f'    <!-- HEAD_BLOCK_{i} -->{block}<!-- /HEAD_BLOCK_{i} -->')     html.append('</head>')     html.append('<body>')     for i, block in enumerate(structure['html']['body']['blocks'], 1):         if block:  # Проверяем, что блок не пустой             html.append(f'    <!-- BODY_BLOCK_{i} -->{block}<!-- /BODY_BLOCK_{i} -->')     html.append('</body>')     html.append('</html>')     return '\n'.join(html)

На выходе получим HTML код работающей игры Змейка:

<html> <head>     <!-- HEAD_BLOCK_1 --><meta charset="UTF-8"><title>Snake Game</title><!-- /HEAD_BLOCK_1 -->     <!-- HEAD_BLOCK_2 --><style>body {color:#000; background:#f0f0f0; display: flex; align-items: center; justify-content: center; height: 100vh; margin: 0; } canvas { border: 1px solid #000; }</style><!-- /HEAD_BLOCK_2 -->     <!-- HEAD_BLOCK_3 --><script>let canvas, ctx, snake, food, score, direction; function init() { canvas = document.createElement('canvas'); ctx = canvas.getContext('2d'); document.body.appendChild(canvas); canvas.width = 400; canvas.height = 400; snake = [{ x: 10, y: 10 }]; food = { x: Math.floor(Math.random() * 20), y: Math.floor(Math.random() * 20) }; score = 0; direction = { x: 0, y: 0 }; window.addEventListener('keydown', changeDirection); gameLoop(); } function gameLoop() { update(); draw(); setTimeout(gameLoop, 100); } function update() { const head = { x: snake[0].x + direction.x, y: snake[0].y + direction.y }; snake.unshift(head); if (head.x === food.x && head.y === food.y) { score++; food = { x: Math.floor(Math.random() * 20), y: Math.floor(Math.random() * 20) }; } else { snake.pop(); } } function draw() { ctx.clearRect(0, 0, canvas.width, canvas.height); for (let segment of snake) { ctx.fillStyle = 'green'; ctx.fillRect(segment.x * 20, segment.y * 20, 20, 20); } ctx.fillStyle = 'red'; ctx.fillRect(food.x * 20, food.y * 20, 20, 20); ctx.fillStyle = 'black'; ctx.fillText('Score: ' + score, 10, 20); } function changeDirection(event) { if (event.key === 'ArrowUp') { direction = { x: 0, y: -1 }; } else if (event.key === 'ArrowDown') { direction = { x: 0, y: 1 }; } else if (event.key === 'ArrowLeft') { direction = { x: -1, y: 0 }; } else if (event.key === 'ArrowRight') { direction = { x: 1, y: 0 }; } } window.onload = init;</script><!-- /HEAD_BLOCK_3 --> </head> <body>     <!-- BODY_BLOCK_1 --><header><h1>Snake Game</h1></header><!-- /BODY_BLOCK_1 -->     <!-- BODY_BLOCK_2 --><main><p>Use arrow keys to move the snake and eat the food!</p></main><!-- /BODY_BLOCK_2 -->     <!-- BODY_BLOCK_3 --><footer><p>© 2024 Snake Game</p></footer><!-- /BODY_BLOCK_3 --> </body> </html>

Игра Змейка

Вот тут можете поиграть в начальную версию игру, описанную в статье: Змейка 1.0

А вот версия игры после нескольких последовательных запросов на улучшение кода: Змейка 3.0

Игра Змейка после тюнинга

Игра Змейка после тюнинга

Ну и как этим пользоваться если я не знаю HTML?

Очень хороший вопрос! Ответ — сделаем Телеграм бота, который может делать все что описано выше и может получать от вас инструкции голосом.

Бота соберем на одной из No-code платформ. В итоге вы получаем такого бота:

Реализация бота, который пишет веб приложения и интегрирован с GitHub

Реализация бота, который пишет веб приложения и интегрирован с GitHub

Возможности бота

  1. Создавать код HTML страниц с поддержкой скриптов

  2. Задачи можно ставить голосом

  3. Результат работы над страницей виден сразу

  4. Если возможность сохранять и загружать код в GitHub

  5. Можно взять код страницы по внешней ссылке и доработать его

Что еще можно создавать ботом?

  1. Любые калькуляторы цен на сайт

  2. Готовые игры для Телеграм MiniApp

  3. Одностраничный сайт

И многое другое …

А посмотреть можно?

Да, процесс работы с ботом показан на видео: Смотреть

Итоги

Если у вас есть идеи по улучшению работы предложенной модели работы ИИ с созданием веб приложений, пишите в комментариях или мне в Телеграм.


ссылка на оригинал статьи https://habr.com/ru/articles/861770/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *