<!DOCTYPE html>
<html lang="ru">
<head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <title>Чат с LM Studio</title>
    <style>
        body { max-width: 700px; margin: 40px auto; font-family: Arial, sans-serif; background: #f5f5f5; }
        h1 { text-align: center; margin-bottom: 20px; }
        #chat { border: 1px solid #bbb; padding: 15px; min-height: 400px; max-height: 600px; overflow-y: auto; background:#fff; border-radius: 5px; box-shadow: 0 0 8px rgba(0,0,0,0.1); }
        .message { margin-bottom: 16px; padding: 10px 15px; border-radius: 8px; max-width: 85%; font-size: 14px; line-height: 1.4; }
        .user { background: #d9eaff; color: #004a99; margin-left: auto; text-align: right; white-space: pre-wrap; word-wrap: break-word; }
        .assistant { background: #e6f4ea; color: #216e37; margin-right: auto; text-align: left; }
        .role-label { font-weight: 600; margin-bottom: 6px; display: block; font-size: 12px; opacity: 0.6; }
        img.chat-image { max-width: 100%; max-height: 280px; border-radius: 6px; margin-top: 8px; display: block; }
        #inputForm { margin-top: 20px; display: flex; flex-direction: column; gap: 8px; }
        textarea { width: 100%; resize: vertical; padding: 10px; font-size: 16px; border: 1px solid #ccc; border-radius: 5px; }
        input[type=file] { cursor: pointer; }
        button { background: #007BFF; color: white; border: none; padding: 12px; cursor: pointer; font-size: 16px; border-radius: 5px; transition: background-color 0.2s ease; }
        button:disabled { background: #a0a0a0; cursor: not-allowed; }
        button:hover:not(:disabled) { background: #0056b3; }
        #errorMsg { color: red; margin: 10px 0; font-size: 14px; }
        #clearHistory { background:#dc3545; margin-top: 10px; }
        #imagePreview { max-width: 200px; max-height: 200px; display: none; margin-top: 10px; border-radius: 5px; }
        /* Новый класс для ответа ассистента */
        .assistant-response {
            margin-top: 8px;
        }
    </style>
    <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
</head>
<body>
<h1>Чат с ИИ</h1>
<div id="chat"></div>

<form id="inputForm">
    <label for="modelSelect">Выберите модель:</label>
    <select id="modelSelect" required style="margin-bottom:10px; padding:6px; font-size:14px;">
        <option disabled selected>Загрузка моделей...</option>
    </select>

    <label for="reasoningSelect">Выберите стиль рассуждения модели:</label>
    <select id="reasoningSelect" required style="margin-bottom:10px; padding:6px; font-size:14px;">
        <option value="" disabled>Выберите...</option>
        <option value="Трезвое и логическое рассуждение без лишних эмоций." selected>Трезвое рассуждение</option>
        <option value="Творческий, креативный и вдохновляющий ответ.">Творческий стиль</option>
        <option value="Подробный, развернутый и детальный ответ.">Подробный ответ</option>
        <option value="Краткий и лаконичный ответ по существу.">Краткий ответ</option>
    </select>

    <textarea id="prompt" rows="3" placeholder="Введите запрос..." required></textarea>
    <input type="file" id="imageInput" accept="image/*" />
    <img id="imagePreview" alt="Preview" />
    <div id="errorMsg"></div>
    <button type="submit">Отправить</button>
    <button type="button" id="clearHistory">Очистить историю</button>
</form>

<script>
    const chatDiv = document.getElementById('chat');
    const form = document.getElementById('inputForm');
    const promptInput = document.getElementById('prompt');
    const imageInput = document.getElementById('imageInput');
    const imagePreview = document.getElementById('imagePreview');
    const errorMsg = document.getElementById('errorMsg');
    const clearBtn = document.getElementById('clearHistory');
    const modelSelect = document.getElementById('modelSelect');
    const reasoningSelect = document.getElementById('reasoningSelect');

    let chatHistory = JSON.parse(localStorage.getItem('chatHistory')) || [];
    renderChat();

    function loadModels() {
        fetch('http://localhost:1234/v1/models')
            .then(res => res.json())
            .then(data => {
                modelSelect.innerHTML = '';
                if (data.data && Array.isArray(data.data)) {
                    data.data.forEach(mdl => {
                        let option = document.createElement('option');
                        option.value = mdl.id;
                        option.textContent = mdl.id;
                        modelSelect.appendChild(option);
                    });
                    modelSelect.selectedIndex = 0;
                } else {
                    modelSelect.innerHTML = '<option disabled>Модели не найдены</option>';
                }
                selectDefaultReasoning();
            })
            .catch(() => {
                modelSelect.innerHTML = '<option disabled>Ошибка загрузки моделей</option>';
                selectDefaultReasoning();
            });
    }
    loadModels();

    function selectDefaultReasoning() {
        const defaultValue = "Трезвое и логическое рассуждение без лишних эмоций.";
        for (let i = 0; i < reasoningSelect.options.length; i++) {
            if (reasoningSelect.options[i].value === defaultValue) {
                reasoningSelect.selectedIndex = i;
                return;
            }
        }
        reasoningSelect.selectedIndex = 0;
    }

    imageInput.addEventListener('change', () => {
        errorMsg.textContent = '';
        const file = imageInput.files[0];
        if (!file) {
            imagePreview.style.display = 'none';
            imagePreview.src = '';
            return;
        }
        if (!file.type.startsWith('image/')) {
            errorMsg.textContent = 'Пожалуйста, выберите корректный файл изображения.';
            imageInput.value = '';
            imagePreview.style.display = 'none';
            return;
        }
        const reader = new FileReader();
        reader.onload = e => {
            imagePreview.src = e.target.result;
            imagePreview.style.display = 'block';
        };
        reader.readAsDataURL(file);
    });

    clearBtn.addEventListener('click', () => {
        if (confirm('Вы действительно хотите очистить историю чата?')) {
            chatHistory = [];
            saveHistory();
            renderChat();
        }
    });

    form.addEventListener('submit', e => {
        e.preventDefault();
        errorMsg.textContent = '';

        const prompt = promptInput.value.trim();
        if (!prompt) {
            errorMsg.textContent = 'Пожалуйста, введите текст запроса.';
            return;
        }
        const model = modelSelect.value;
        if (!model) {
            errorMsg.textContent = 'Пожалуйста, выберите модель.';
            return;
        }
        const reasoning = reasoningSelect.value;
        if (!reasoning) {
            errorMsg.textContent = 'Пожалуйста, выберите стиль рассуждения модели.';
            return;
        }

        const file = imageInput.files[0];

        appendMessage({role: 'user', content: prompt});
        saveHistory();
        form.querySelector('button[type="submit"]').disabled = true;

        if (file) {
            const reader = new FileReader();
            reader.onload = e => {
                const base64Img = e.target.result;
                sendRequest(prompt, base64Img, model, reasoning);
            };
            reader.readAsDataURL(file);
        } else {
            sendRequest(prompt, null, model, reasoning);
        }
    });

    function createFragmentFromHTML(html) {
        const template = document.createElement('template');
        template.innerHTML = html.trim();
        return template.content;
    }

    function parseTemplateWithImages(text, images) {
        const turns = [];
        const turnRegex = /<start_of_turn>(user|model)\n([\s\S]*?)<end_of_turn>/g;
        let match;
        let imageIndex = 0;

        while ((match = turnRegex.exec(text)) !== null) {
            const roleRaw = match[1];
            let contentRaw = match[2].trim();

            const parts = contentRaw.split('<start_of_image>');
            const contentParts = [];

            parts.forEach((part, idx) => {
                if (part.trim()) {
                    if (roleRaw === 'model') {
                        const html = marked.parse(part);
                        contentParts.push({type: 'html', html});
                    } else {
                        contentParts.push({type: 'text', text: part});
                    }
                }
                if (idx < parts.length - 1) {
                    const img = (images && images[imageIndex]) ? images[imageIndex] : null;
                    contentParts.push({type: 'image', img});
                    imageIndex++;
                }
            });

            turns.push({
                role: roleRaw === 'user' ? 'user' : 'assistant',
                contentParts: contentParts
            });
        }

        if (turns.length === 0) {
            turns.push({
                role: 'assistant',
                contentParts: [{ type: 'html', html: marked.parse(text.trim()) }]
            });
        }
        return turns;
    }

    function sendRequest(prompt, base64Image, model, reasoning) {
        let messagesContent = [{type:"text", text: prompt}];
        if (base64Image) {
            messagesContent.push({
                type: "image_url",
                image_url: {url: base64Image}
            });
        }
        const payload = {
            model: model,
            messages: [
                {
                    role: "system",
                    content: reasoning
                },
                {
                    role: "user",
                    content: messagesContent
                }
            ],
            temperature: 0.7,
            max_new_tokens: 100
        };

        fetch('generate.php', {
            method: 'POST',
            headers: {'Content-Type': 'application/json'},
            body: JSON.stringify(payload)
        })
            .then(res => res.json())
            .then(data => {
                form.querySelector('button[type="submit"]').disabled = false;
                if (data.error) {
                    errorMsg.textContent = "Ошибка: " + data.error;
                    return;
                }
                if (data.reply) {
                    const parsedTurns = parseTemplateWithImages(data.reply, data.images || []);
                    parsedTurns.forEach(turn => {
                        if (turn.role === 'assistant') {
                            turn.reasoning = reasoning;
                            turn.model = model;  // Добавление модели в сообщение
                        }
                        appendMessage(turn);
                    });
                    saveHistory();
                    renderChat();
                    promptInput.value = '';
                    imageInput.value = '';
                    imagePreview.style.display = 'none';
                    selectDefaultReasoning();
                } else {
                    errorMsg.textContent = 'Ответ от модели не получен.';
                }
            })
            .catch(err => {
                form.querySelector('button[type="submit"]').disabled = false;
                errorMsg.textContent = 'Ошибка сети или сервера: ' + err;
            });
    }

    function appendMessage(msg) {
        chatHistory.push(msg);
        renderChat();
        saveHistory();
    }

    function renderChat() {
        chatDiv.innerHTML = '';
        chatHistory.forEach(msg => {
            const div = document.createElement('div');
            div.className = 'message ' + (msg.role === 'user' ? 'user' : 'assistant');
            const roleLabel = msg.role === 'user' ? 'Пользователь' : 'Модель';

            const label = document.createElement('span');
            label.className = 'role-label';
            label.textContent = roleLabel + ':';
            div.appendChild(label);

            if (msg.role === 'assistant' && msg.model) {
                const modelDiv = document.createElement('div');
                modelDiv.style.fontSize = '11px';
                modelDiv.style.opacity = '0.6';
                modelDiv.style.margin = '0 0 6px 0';
                modelDiv.style.fontWeight = '600';
                modelDiv.textContent = `Модель: ${msg.model}`;
                div.appendChild(modelDiv);
            }

            if (msg.role === 'assistant' && msg.reasoning) {
                const reasoningDiv = document.createElement('div');
                reasoningDiv.style.fontSize = '11px';
                reasoningDiv.style.opacity = '0.6';
                reasoningDiv.style.margin = '-6px 0 6px 0';
                reasoningDiv.style.fontStyle = 'italic';
                reasoningDiv.textContent = `Стиль рассуждения: ${msg.reasoning}`;
                div.appendChild(reasoningDiv);
            }

            if (msg.contentParts) {
                const responseWrapper = document.createElement('div');
                responseWrapper.className = 'assistant-response';

                msg.contentParts.forEach(cp => {
                    if (cp.type === 'html') {
                        const fragment = createFragmentFromHTML(cp.html);
                        responseWrapper.appendChild(fragment);
                    } else if (cp.type === 'image') {
                        if (cp.img) {
                            const img = document.createElement('img');
                            img.className = 'chat-image';
                            img.src = cp.img;
                            responseWrapper.appendChild(img);
                        } else {
                            const span = document.createElement('span');
                            span.style.fontStyle = 'italic';
                            span.style.color = '#888';
                            span.textContent = '[Изображение]';
                            responseWrapper.appendChild(span);
                        }
                    } else if (cp.type === 'text') {
                        const span = document.createElement('span');
                        span.innerHTML = escapeHtml(cp.text).replace(/\n/g, '<br>');
                        responseWrapper.appendChild(span);
                    }
                });

                div.appendChild(responseWrapper);
            } else if (msg.content) {
                const responseWrapper = document.createElement('div');
                responseWrapper.className = 'assistant-response';
                responseWrapper.textContent = msg.content;
                div.appendChild(responseWrapper);
            }
            chatDiv.appendChild(div);
        });
        chatDiv.scrollTop = chatDiv.scrollHeight;
    }

    function saveHistory() {
        localStorage.setItem('chatHistory', JSON.stringify(chatHistory));
    }

    function escapeHtml(text) {
        if (!text) return '';
        return text.replace(/[&<>"']/g, function(m) {
            const map = {'&':'&amp;', '<':'&lt;', '>':'&gt;', '"':'&quot;', "'":'&#039;'};
            return map[m];
        });
    }
</script>
</body>
</html> нужно запретить выбирать модель. но при этом показывать загруженную модель или заглушку. если модель не загружена.