Добавь дополнительные Рекомендации:
Поддерживаемые языки для подсветки кода:
В текущей реализации поддерживаются python, javascript, и json. При необходимости можно добавить поддержку других языков, расширив соответствующую логику в функции highlight_code.
Более продвинутая обработка кода:
Для улучшения пользовательского опыта можно интегрировать более сложные механизмы интерактивности внутри блоков кода, например, выполнение кода или расширенные возможности редактирования.
Сохранение настроек:
Для удобства пользователей можно реализовать сохранение выбранных настроек интерфейса (цвет, шрифт) между сессиями с использованием файлов конфигурации или базы данных.
Дополнение функционала экспорта:
Добавьте возможность экспорта чата в PDF с сохранением форматирования и подсветки кода с помощью дополнительных библиотек, таких как WeasyPrint или pdfkit.
Локализация и международная поддержка:
Если планируется поддержка нескольких языков, можно добавить возможность переключения языка интерфейса.
Безопасность:
Убедитесь, что входные данные от пользователя проходят необходимую валидацию и обработку для предотвращения возможных атак, связанных с обработкой HTML-контента.
```python
import sys
import re
import requests
from PyQt5.QtWidgets import (
    QApplication, QWidget, QVBoxLayout, QHBoxLayout, QTextEdit,
    QLineEdit, QPushButton, QLabel, QFileDialog, QMessageBox,
    QDialog, QScrollArea, QFrame, QAction, QMenu, QInputDialog, QColorDialog, QFontDialog
)
from PyQt5.QtCore import Qt, QObject, pyqtSignal, QThread
from PyQt5.QtGui import QFont, QIcon, QTextCursor, QColor
from gradio_client import Client
import markdown
from pygments import highlight
from pygments.lexers import PythonLexer, JavascriptLexer, JsonLexer, TextLexer
from pygments.formatters import HtmlFormatter

# Инициализация клиента для API
client = Client("yuntian-deng/o1mini")


def highlight_code(text, language='python'):
    """
    Подсвечивает синтаксис кода.
    """
    lexer = TextLexer()
    if language.lower() == 'python':
        lexer = PythonLexer()
    elif language.lower() == 'javascript':
        lexer = JavascriptLexer()
    elif language.lower() == 'json':
        lexer = JsonLexer()

    formatter = HtmlFormatter(style="friendly")
    highlighted = highlight(text, lexer, formatter)
    return highlighted


class Worker(QObject):
    """
    Рабочий объект для выполнения API-запросов в отдельном потоке.
    """
    finished = pyqtSignal(str)
    error = pyqtSignal(str)

    def __init__(self, message, chat_history, chat_counter):
        super().__init__()
        self.message = message
        self.chat_history = chat_history
        self.chat_counter = chat_counter

    def run(self):
        try:
            result = client.predict(
                inputs=self.message,
                top_p=1,
                temperature=1,
                chat_counter=self.chat_counter,
                chatbot=self.chat_history,
                api_name="/predict"
            )

            if len(result) < 4:
                raise ValueError("Некорректный формат ответа от API.")

            chatbot_history, new_chat_counter, status_code, reset_text = result

            if not isinstance(chatbot_history, (list, tuple)):
                raise TypeError("chatbot_history должно быть списком или кортежем.")

            if len(chatbot_history) == 0:
                chatbot_response = "Нет ответа от бота."
            else:
                latest_entry = chatbot_history[-1]
                if isinstance(latest_entry, (list, tuple)) and len(latest_entry) == 2:
                    chatbot_response = latest_entry[1]
                else:
                    chatbot_response = "Не удалось получить ответ бота."

            self.finished.emit(chatbot_response)

        except Exception as e:
            self.error.emit(str(e))


class ChatBot(QWidget):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("Чат-бот на основе API `yuntian-deng/o1mini`")
        self.setGeometry(100, 100, 900, 700)

        self.chat_history = []
        self.chat_counter = 0

        self.init_ui()

    def init_ui(self):
        main_layout = QVBoxLayout()

        # Меню
        self.create_menu()

        # Заголовок
        title = QLabel("Чат-бот на основе API `yuntian-deng/o1mini`")
        title.setFont(QFont("Arial", 20))
        title.setAlignment(Qt.AlignCenter)
        main_layout.addWidget(title)

        # Область чата с прокруткой
        self.chat_area = QTextEdit()
        self.chat_area.setReadOnly(True)
        self.chat_area.setFont(QFont("Arial", 12))
        self.chat_area.setStyleSheet("""
            QTextEdit {
                background-color: #f9f9f9;
                border: 1px solid #ccc;
                border-radius: 5px;
                padding: 10px;
            }
        """)

        scroll = QScrollArea()
        scroll.setWidgetResizable(True)
        scroll.setWidget(self.chat_area)
        scroll.setFrameStyle(QFrame.NoFrame)
        main_layout.addWidget(scroll)

        # Поле ввода сообщения и подсчёт символов и токенов
        input_container = QWidget()
        input_layout = QVBoxLayout()

        self.input_field = QTextEdit()
        self.input_field.setPlaceholderText("Введите сообщение и нажмите Enter")
        self.input_field.setFixedHeight(100)
        self.input_field.setFont(QFont("Arial", 12))
        self.input_field.setStyleSheet("""
            QTextEdit {
                padding: 8px;
                border: 1px solid #ccc;
                border-radius: 5px;
            }
        """)
        self.input_field.installEventFilter(self)
        input_layout.addWidget(self.input_field)

        counters_layout = QHBoxLayout()
        self.char_count_label = QLabel("Символы: 0")
        self.token_count_label = QLabel("Токены: 0")
        counters_layout.addWidget(self.char_count_label)
        counters_layout.addWidget(self.token_count_label)
        counters_layout.addStretch()
        input_layout.addLayout(counters_layout)

        input_container.setLayout(input_layout)
        main_layout.addWidget(input_container)

        # Кнопка отправки и индикатор статуса
        send_layout = QHBoxLayout()
        self.send_button = QPushButton("Отправить")
        self.send_button.setIcon(QIcon.fromTheme("send"))
        self.send_button.clicked.connect(self.send_message)
        send_layout.addWidget(self.send_button)

        self.status_label = QLabel("")
        self.status_label.setStyleSheet("color: green;")
        send_layout.addWidget(self.status_label)

        main_layout.addLayout(send_layout)

        # Кнопки дополнительных функций
        func_layout = QHBoxLayout()

        self.reset_btn = QPushButton("Сбросить чат")
        self.reset_btn.setIcon(QIcon.fromTheme("edit-clear"))
        self.reset_btn.clicked.connect(self.reset_chat)
        func_layout.addWidget(self.reset_btn)

        self.export_btn = QPushButton("Экспортировать чат")
        self.export_btn.setIcon(QIcon.fromTheme("document-save"))
        self.export_btn.clicked.connect(self.export_chat)
        func_layout.addWidget(self.export_btn)

        self.fact_btn = QPushButton("Случайный факт")
        self.fact_btn.setIcon(QIcon.fromTheme("information"))
        self.fact_btn.clicked.connect(self.get_random_fact)
        func_layout.addWidget(self.fact_btn)

        main_layout.addLayout(func_layout)

        # Информация о ошибках
        self.info_label = QLabel("")
        self.info_label.setStyleSheet("color: red;")
        main_layout.addWidget(self.info_label)

        # Применение стилей
        self.setStyleSheet("""
            QWidget {
                background-color: #e6e6e6;
            }
            QPushButton {
                padding: 8px 16px;
                border: none;
                background-color: #4CAF50;
                color: white;
                border-radius: 5px;
            }
            QPushButton:hover {
                background-color: #45a049;
            }
            QLabel {
                font-size: 12px;
            }
        """)

        self.setLayout(main_layout)

    def create_menu(self):
        """
        Создаёт меню для настроек.
        """
        menubar = self.menuBar if hasattr(self, 'menuBar') else QMenu()
        if isinstance(menubar, QMenu):
            # Если нет меню-бару, добавим меню через контекстное меню
            self.setContextMenuPolicy(Qt.CustomContextMenu)
            self.customContextMenuRequested.connect(lambda pos: self.show_menu(pos))
            self.menu = QMenu(self)
            settings_action = QAction("Настройки", self)
            settings_action.triggered.connect(self.open_settings)
            self.menu.addAction(settings_action)
        else:
            file_menu = menubar.addMenu('Настройки')
            settings_action = QAction('Настройки', self)
            settings_action.triggered.connect(self.open_settings)
            file_menu.addAction(settings_action)

    def show_menu(self, pos):
        """
        Показывает контекстное меню.
        """
        self.menu.exec_(self.mapToGlobal(pos))

    def open_settings(self):
        """
        Открывает диалог настроек.
        """
        settings_dialog = SettingsDialog(self)
        settings_dialog.exec_()

    def append_message(self, sender, message):
        """
        Добавляет сообщение в чат. Обрабатывает текст и блоки кода.
        """
        # Регулярное выражение для поиска блоков кода
        code_pattern = re.compile(r'```(\w+)?\n(.*?)```', re.DOTALL)
        parts = code_pattern.split(message)

        # Проход по частям сообщения
        for i in range(0, len(parts), 3):
            text = parts[i].strip()
            if text:
                formatted_text = self.format_text(sender, text)
                self.chat_area.append(formatted_text)

            # Проверяем наличие блока кода
            if i + 2 < len(parts):
                language = parts[i + 1] if parts[i + 1] else 'python'
                code = parts[i + 2].strip()
                highlighted_code = highlight_code(code, language)
                formatted_code = f"""
                <div style="background-color:#f4f4f4; padding:10px; border-radius:5px; position:relative;">
                    {highlighted_code}
                    <button onclick="copyToClipboard(this)" style="position:absolute; top:5px; right:5px; padding:2px 5px; font-size:10px;">Копировать</button>
                </div>
                <script>
                function copyToClipboard(button) {{
                    var code = button.parentElement.querySelector('pre').innerText;
                    navigator.clipboard.writeText(code).then(function() {{
                        button.textContent = 'Скопировано!';
                        setTimeout(function() {{
                            button.textContent = 'Копировать';
                        }}, 2000);
                    }}, function(err) {{
                        alert('Не удалось скопировать код');
                    }});
                }}
                </script>
                """
                self.chat_area.append(formatted_code)

        self.chat_area.moveCursor(QTextCursor.End)

    def format_text(self, sender, text):
        """
        Форматирует текстовое сообщение.
        """
        if sender == "Вы":
            align = 'right'
            color = '#0084ff'  # Синий для пользователя
        else:
            align = 'left'
            color = '#333333'  # Тёмно-серый для бота

        formatted = f"<p style='text-align: {align};'><span style='color: {color};'><b>{sender}:</b> {self.escape_html(text)}</span></p>"
        return formatted

    def escape_html(self, text):
        """
        Экранирует специальные HTML символы.
        """
        text = text.replace('&', '&amp;')
        text = text.replace('<', '&lt;')
        text = text.replace('>', '&gt;')
        return text

    def update_counters(self):
        """
        Обновляет подсчёт символов и токенов.
        """
        text = self.input_field.toPlainText()
        char_count = len(text)
        token_count = char_count // 4  # 1 токен = 4 символа
        self.char_count_label.setText(f"Символы: {char_count}")
        self.token_count_label.setText(f"Токены: {token_count}")

    def send_message(self):
        """
        Обрабатывает отправку сообщения.
        """
        user_message = self.input_field.toPlainText().strip()
        if not user_message:
            return

        self.append_message("Вы", user_message)
        self.chat_history.append((user_message, ""))
        self.input_field.clear()
        self.update_counters()

        # Показываем статус "Бот набирает..."
        self.status_label.setText("Бот набирает ответ...")
        QApplication.processEvents()

        # Запускаем рабочий поток для асинхронного запроса
        self.thread = QThread()
        self.worker = Worker(user_message, self.chat_history, self.chat_counter)
        self.worker.moveToThread(self.thread)
        self.thread.started.connect(self.worker.run)
        self.worker.finished.connect(self.handle_response)
        self.worker.error.connect(self.handle_error)
        self.worker.finished.connect(self.thread.quit)
        self.worker.finished.connect(self.worker.deleteLater)
        self.thread.finished.connect(self.thread.deleteLater)
        self.thread.start()

    def handle_response(self, response):
        """
        Обрабатывает ответ от бота.
        """
        self.status_label.setText("")
        self.chat_counter += 1
        self.chat_history[-1] = (self.chat_history[-1][0], response)
        self.append_message("Бот", response)

    def handle_error(self, error_message):
        """
        Обрабатывает ошибку при запросе.
        """
        self.status_label.setText("")
        QMessageBox.critical(self, "Ошибка", f"Не удалось отправить сообщение:\n{error_message}")

    def reset_chat(self):
        """
        Сбрасывает историю чата.
        """
        confirmation = QMessageBox.question(
            self, "Сбросить чат",
            "Вы уверены, что хотите сбросить историю чата?",
            QMessageBox.Yes | QMessageBox.No
        )
        if confirmation == QMessageBox.Yes:
            self.chat_history = []
            self.chat_counter = 0
            self.chat_area.clear()
            QMessageBox.information(self, "Чат сброшен", "История чата была сброшена.")

    def export_chat(self):
        """
        Экспортирует историю чата в файл.
        """
        if not self.chat_history:
            QMessageBox.warning(self, "Экспорт", "Нет истории чата для экспорта.")
            return

        options = QFileDialog.Options()
        file_path, _ = QFileDialog.getSaveFileName(
            self, "Сохранить чат", "", "Текстовые файлы (*.txt);;HTML файлы (*.html)", options=options
        )
        if file_path:
            try:
                if file_path.endswith('.html'):
                    with open(file_path, "w", encoding="utf-8") as file:
                        file.write("<html><head><meta charset='UTF-8'><title>Чат</title></head><body>")
                        for user_msg, bot_msg in self.chat_history:
                            file.write(f"<p style='text-align: right;'><b>Вы:</b> {self.escape_html(user_msg)}</p>")
                            # Обработка возможных блоков кода в сообщении бота
                            code_pattern = re.compile(r'```(\w+)?\n(.*?)```', re.DOTALL)
                            parts = code_pattern.split(bot_msg)
                            for i in range(0, len(parts), 3):
                                text = parts[i].strip()
                                if text:
                                    file.write(f"<p style='text-align: left;'><b>Бот:</b> {self.escape_html(text)}</p>")
                                if i + 2 < len(parts):
                                    language = parts[i + 1] if parts[i + 1] else 'python'
                                    code = parts[i + 2].strip()
                                    highlighted_code = highlight_code(code, language)
                                    file.write(f"<pre style='background-color:#f4f4f4; padding:10px; border-radius:5px;'>{highlighted_code}</pre>")
                        file.write("</body></html>")
                else:
                    with open(file_path, "w", encoding="utf-8") as file:
                        for user_msg, bot_msg in self.chat_history:
                            file.write(f"Вы: {user_msg}\nБот: {bot_msg}\n\n")
                QMessageBox.information(self, "Экспорт", f"Чат успешно сохранен в {file_path}")
            except Exception as e:
                QMessageBox.critical(self, "Ошибка", f"Не удалось сохранить чат:\n{e}")

    def get_random_fact(self):
        """
        Получает случайный факт и добавляет его в чат.
        """
        try:
            response = requests.get("https://uselessfacts.jsph.pl/random.json?language=ru", timeout=5)
            if response.status_code == 200:
                fact = response.json().get("text", "Не удалось получить факт.")
            else:
                fact = "Ошибка при получении факта."
        except Exception as e:
            fact = f"Ошибка при получении факта: {e}"

        self.append_message("Бот", fact)

    def eventFilter(self, obj, event):
        """
        Обрабатывает события для поля ввода сообщения.
        Позволяет использовать Shift + Enter для вставки новой строки.
        """
        if obj == self.input_field:
            if event.type() == event.KeyPress:
                if event.key() == Qt.Key_Return or event.key() == Qt.Key_Enter:
                    if event.modifiers() == Qt.ShiftModifier:
                        cursor = self.input_field.textCursor()
                        cursor.insertBlock()
                        return True
                    else:
                        self.send_message()
                        return True
        return super().eventFilter(obj, event)


class SettingsDialog(QDialog):
    def __init__(self, parent=None):
        super().__init__(parent)
        self.setWindowTitle("Настройки")
        self.setFixedSize(400, 300)
        self.init_ui()

    def init_ui(self):
        layout = QVBoxLayout()

        # Выбор цвета чата
        color_btn = QPushButton("Выбрать цвет фона чата")
        color_btn.clicked.connect(self.choose_color)
        layout.addWidget(color_btn)

        # Выбор шрифта
        font_btn = QPushButton("Выбрать шрифт чата")
        font_btn.clicked.connect(self.choose_font)
        layout.addWidget(font_btn)

        # Добавьте дополнительные настройки здесь

        self.setLayout(layout)

    def choose_color(self):
        color = QColorDialog.getColor()
        if color.isValid():
            self.parent().chat_area.setStyleSheet(f"""
                QTextEdit {{
                    background-color: {color.name()};
                    border: 1px solid #ccc;
                    border-radius: 5px;
                    padding: 10px;
                }}
            """)

    def choose_font(self):
        font, ok = QFontDialog.getFont()
        if ok:
            self.parent().chat_area.setFont(font)


def main():
    app = QApplication(sys.argv)
    chatbot = ChatBot()
    chatbot.show()
    sys.exit(app.exec_())


if __name__ == "__main__":
    main()
```