Как стать автором
Обновить

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

Уровень сложностиСредний
Время на прочтение10 мин
Количество просмотров3.3K

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

Во первых это настоящий вызов современным 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. Одностраничный сайт

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

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

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

Итоги

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

Теги:
Хабы:
Если эта публикация вас вдохновила и вы хотите поддержать автора — не стесняйтесь нажать на кнопку
-1
Комментарии6

Публикации

Истории

Работа

Веб дизайнер
21 вакансия

Ближайшие события