Project structure:

D1_1/
├── static/
│   ├── css/
│   │   └── styles.css
│   ├── js/
│   │   └── main.js
├── templates/
│   └── index.html
├── app.py
├── chats.db
├── database.py
├── main.py
└── requirements.txt


Project files:

app.py
```py

# app.py

import os
import json
import threading

import eventlet
import httpx
import openai

from flask import Flask, render_template, request
from flask_socketio import SocketIO, emit
import webview
from openai import OpenAI

from database import ChatDatabase

# Инициализация Flask приложения
app = Flask(__name__)
app.config['SECRET_KEY'] = 'your_secret_key'
socketio = SocketIO(app, async_mode='eventlet')

# Инициализация базы данных
db = ChatDatabase()

# Инициализация OpenAI API
OPENAI_API_KEY = "<TRUFFLEHOG_REDACTED_OPENAI>"
if not OPENAI_API_KEY:
    raise ValueError("OpenAI API ключ не установлен. Пожалуйста, установите переменную окружения OPENAI_API_KEY.")


# Разбиваем строку с прокси на части
proxy_parts = "socks5://185.106.94.81:44590".split(":")
proxy_host = proxy_parts[1][2:]  # Убираем "//" из начала хоста
proxy_port = int(proxy_parts[2])

# Создаем HTTP-клиент с прокси
client = OpenAI(
  api_key=OPENAI_API_KEY,
  http_client=httpx.Client(
      proxies="socks5://" + proxy_host + ":" + str(proxy_port),
      verify=False # Добавляем проверку сертификата
  )
)

# Маршруты Flask
@app.route('/')
def index():
    return render_template('index.html')


# Обработчики SocketIO
@socketio.on('connect')
def handle_connect():
    print('Client connected')
    chats = db.get_chats()
    emit('chats_list', {'chats': chats})


@socketio.on('create_chat')
def handle_create_chat(data):
    chat_name = data.get('name', 'Новый чат')
    chat_id = db.create_chat(chat_name)
    emit('chat_created', {'chat_id': chat_id, 'chat_name': chat_name}, broadcast=True)


@socketio.on('get_messages')
def handle_get_messages(data):
    chat_id = data.get('chat_id')
    messages = db.get_messages(chat_id)
    emit('chat_messages', {'messages': messages})


@socketio.on('send_message')
def handle_send_message(data):
    chat_id = data.get('chat_id')
    message_content = data.get('content')

    if not chat_id:
        emit('error', {'message': 'Chat ID is missing.'}, room=request.sid)
        return

    if not message_content:
        emit('error', {'message': 'Message content is empty.'}, room=request.sid)
        return

    # Save the user's message to the database
    db.save_message(chat_id, 'user', message_content)
    emit('new_message', {'sender': 'user', 'content': message_content}, room=request.sid)

    try:
        # Fetch all previous messages for the chat
        messages = db.get_messages(chat_id)

        # Format messages for OpenAI API
        formatted_messages = []
        for sender, content in messages:
            role = 'user' if sender == 'user' else 'assistant'
            formatted_messages.append({'role': role, 'content': content})

        # Append the new user message
        formatted_messages.append({'role': 'user', 'content': message_content})

        # Request a response from OpenAI API with the conversation history
        response = client.chat.completions.create(
            model="gpt-4o-mini",  # Make sure to use the correct model name
            messages=formatted_messages,
            stream=True
        )

        # Stream the response back to the client
        bot_message = ''
        for chunk in response:
            if chunk.choices[0].delta.content:
                content = chunk.choices[0].delta.content
                bot_message += content
                emit('stream_message', {'content': content}, room=request.sid)
                eventlet.sleep(0.01)  # Slight delay for smooth streaming

        # Save the bot's response to the database
        db.save_message(chat_id, 'bot', bot_message)

    except openai.OpenAIError as e:
        emit('error', {'message': str(e)}, room=request.sid)


@socketio.on('regenerate_message')
def handle_regenerate_message(data):
    chat_id = data.get('chat_id')
    message_id = data.get('message_id')  # Assuming you have a unique message ID

    if not chat_id or not message_id:
        emit('error', {'message': 'Chat ID or Message ID is missing.'}, room=request.sid)
        return

    # Delete messages from the specified message_id onwards
    db.delete_messages_from(chat_id, message_id)

    # Fetch all messages up to before the deleted ones
    messages = db.get_messages(chat_id)

    # Format messages for OpenAI API
    formatted_messages = []
    for sender, content in messages:
        role = 'user' if sender == 'user' else 'assistant'
        formatted_messages.append({'role': role, 'content': content})

    try:
        # Request a new response from OpenAI API with the updated conversation history
        response = client.chat.completions.create(
            model="gpt-4",
            messages=formatted_messages,
            stream=True
        )

        # Stream the new response back to the client
        bot_message = ''
        for chunk in response:
            if chunk.choices[0].delta.content:
                content = chunk.choices[0].delta.content
                bot_message += content
                emit('stream_message', {'content': content}, room=request.sid)
                eventlet.sleep(0.01)

        # Save the new bot message to the database
        db.save_message(chat_id, 'bot', bot_message)

    except openai.OpenAIError as e:
        emit('error', {'message': str(e)}, room=request.sid)


@socketio.on('delete_chat')
def handle_delete_chat(data):
    chat_id = data.get('chat_id')
    db.delete_chat(chat_id)
    emit('chat_deleted', {'chat_id': chat_id}, broadcast=True)

def run_webview():
    window = webview.create_window('Chatbot', 'http://localhost:5002', width=800, height=600)
    webview.start(debug=True)
    socketio.stop()

def run_flask_app():
    socketio.run(app, port=5002)

if __name__ == '__main__':
    flask_thread = threading.Thread(target=run_flask_app)
    flask_thread.start()
    run_webview()
    flask_thread.join()

```


database.py
```py

# database.py

import sqlite3
from datetime import datetime

class ChatDatabase:
    def __init__(self, db_name='chats.db'):
        self.connection = sqlite3.connect(db_name, check_same_thread=False)
        self.create_tables()

    def create_tables(self):
        cursor = self.connection.cursor()
        cursor.execute('''
            CREATE TABLE IF NOT EXISTS chats (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                name TEXT,
                created_at TIMESTAMP
            )
        ''')
        cursor.execute('''
            CREATE TABLE IF NOT EXISTS messages (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                chat_id INTEGER,
                sender TEXT,
                content TEXT,
                timestamp TIMESTAMP,
                FOREIGN KEY(chat_id) REFERENCES chats(id)
            )
        ''')
        self.connection.commit()

    def create_chat(self, name="Новый чат"):
        cursor = self.connection.cursor()
        cursor.execute('INSERT INTO chats (name, created_at) VALUES (?, ?)', (name, datetime.now()))
        self.connection.commit()
        return cursor.lastrowid

    def get_chats(self):
        cursor = self.connection.cursor()
        cursor.execute('SELECT id, name FROM chats ORDER BY created_at DESC')
        return cursor.fetchall()

    def save_message(self, chat_id, sender, content):
        cursor = self.connection.cursor()
        cursor.execute('INSERT INTO messages (chat_id, sender, content, timestamp) VALUES (?, ?, ?, ?)',
                       (chat_id, sender, content, datetime.now()))
        self.connection.commit()

    def get_messages(self, chat_id):
        cursor = self.connection.cursor()
        cursor.execute('''
            SELECT sender, content 
            FROM messages 
            WHERE chat_id=? 
            ORDER BY timestamp ASC
        ''', (chat_id,))
        return cursor.fetchall()

    def delete_messages_from(self, chat_id, message_id):
        cursor = self.connection.cursor()
        cursor.execute('DELETE FROM messages WHERE chat_id=? AND id>=?', (chat_id, message_id))
        self.connection.commit()

    def delete_chat(self, chat_id):
        cursor = self.connection.cursor()
        cursor.execute('DELETE FROM messages WHERE chat_id=?', (chat_id,))
        cursor.execute('DELETE FROM chats WHERE id=?', (chat_id,))
        self.connection.commit()

```


main.py
```py
import httpx
from openai import OpenAI

OPENAI_API_KEY = "<TRUFFLEHOG_REDACTED_OPENAI>"

# Разбиваем строку с прокси на части
proxy_parts = "socks5://185.106.94.81:44590".split(":")
proxy_host = proxy_parts[1][2:]  # Убираем "//" из начала хоста
proxy_port = int(proxy_parts[2])

# Создаем HTTP-клиент с прокси
client = OpenAI(
  api_key=OPENAI_API_KEY,
  http_client=httpx.Client(
      proxies="socks5://" + proxy_host + ":" + str(proxy_port),
      verify=False # Добавляем проверку сертификата
  )
)

completion = client.chat.completions.create(
  model="gpt-4o-mini",
  messages=[
    {"role": "system", "content": "You are a poetic assistant, skilled in explaining complex programming concepts with creative flair."},
    {"role": "user", "content": "Привет"}
  ]
)

print(completion.choices[0].message.content)
```


requirements.txt
```txt
Flask==2.2.5
flask_socketio==5.3.3
python-socketio==5.8.0
eventlet==0.33.3
pywebview
openai[datalib]
httpx==0.24.1
Jinja2==3.1.2
httpx[socks]
```


static/css/styles.css
```css

/* static/css/styles.css */

body {
    margin: 0;
    font-family: Arial, sans-serif;
}

.container {
    display: flex;
    height: 100vh;
}

.sidebar {
    width: 250px;
    background-color: #f5f5f5;
    padding: 10px;
    box-sizing: border-box;
    border-right: 1px solid #ddd;
}

.sidebar #new-chat {
    width: 100%;
    padding: 10px;
    margin-bottom: 10px;
    border: none;
    background-color: #4CAF50;
    color: white;
    cursor: pointer;
    border-radius: 5px;
}

.sidebar ul {
    list-style: none;
    padding: 0;
}

.sidebar ul li {
    padding: 10px;
    border-bottom: 1px solid #ddd;
    cursor: pointer;
    min-width: 150px;
}

.chat-container {
    flex-grow: 1;
    display: flex;
    flex-direction: column;
}

.messages {
    flex-grow: 1;
    padding: 20px;
    overflow-y: auto;
}

.message-container {
    margin-bottom: 15px;
    position: relative;
}

.message-container:hover .message-actions {
    display: block;
}

.message-actions {
    position: absolute;
    top: 0;
    right: 0;
    display: none;
}

.message-actions button {
    margin-left: 5px;
    padding: 5px;
    cursor: pointer;
}

.message {
    display: inline-block;
    max-width: 70%;
    padding: 10px;
    border-radius: 10px;
}

.message.user {
    background-color: #dcf8c6;
    align-self: flex-end;
    text-align: right;
}

.message.bot {
    background-color: #fff;
    border: 1px solid #ddd;
}

.input-container {
    padding: 10px;
    border-top: 1px solid #ddd;
}

#message-input {
    width: 100%;
    padding: 15px;
    border-radius: 100px;
    border: 1px solid #ddd;
    box-sizing: border-box;
    font-size: 16px;
}

code {
    background-color: #f5f5f5;
    padding: 2px 4px;
    border-radius: 4px;
}

pre {
    background-color: #f5f5f5;
    padding: 10px;
    border-radius: 5px;
    overflow-x: auto;
}

```


static/js/main.js
```js

// static/js/main.js

$(document).ready(function() {
    const socket = io.connect();
    let currentChatId = null;

    socket.on('connect', function() {
        console.log('Connected to server');
    });

    socket.on('chats_list', function(data) {
        updateChatList(data.chats);
    });

    socket.on('chat_created', function(data) {
        addChatToList(data.chat_id, data.chat_name);
    });

    socket.on('chat_deleted', function(data) {
        removeChatFromList(data.chat_id);
    });

    socket.on('chat_messages', function(data) {
        displayMessages(data.messages);
    });

    socket.on('new_message', function(data) {
        addMessage(data.sender, data.content);
    });

    socket.on('stream_message', function(data) {
        appendToLastMessage(data.content);
    });

    $('#new-chat').click(function() {
        socket.emit('create_chat', { name: 'Новый чат' });
    });

    $('#message-input').keypress(function(e) {
        if (e.which == 13 && !e.shiftKey) {
            e.preventDefault();
            sendMessage();
        }
    });

    $('#chat-list').on('click', 'li', function() {
        currentChatId = $(this).data('chat-id');
        socket.emit('get_messages', { chat_id: currentChatId });
    });

    function updateChatList(chats) {
        $('#chat-list').empty();
        chats.forEach(function(chat) {
            addChatToList(chat[0], chat[1]);
        });
    }

    function addChatToList(chatId, chatName) {
        $('#chat-list').append(`<li data-chat-id="${chatId}">${chatName}</li>`);
    }

    function removeChatFromList(chatId) {
        $(`#chat-list li[data-chat-id="${chatId}"]`).remove();
    }

    function displayMessages(messages) {
        $('#messages').empty();
        messages.forEach(function(message) {
            addMessage(message[0], message[1]);
        });
    }

function addMessage(sender, content) {
    const messageClass = sender === 'user' ? 'user' : 'bot';
    let formattedContent = content;

    if (sender === 'bot') {
        // Преобразуем Markdown в HTML
        formattedContent = marked.parse(content);
    } else {
        // Экранируем HTML, чтобы пользователь не мог вставить небезопасный код
        formattedContent = $('<div>').text(content).html();
    }

    $('#messages').append(`<div class="message-container">
        <div class="message ${messageClass}">${formattedContent}</div>
        <div class="message-actions">
            <button class="edit-message">Изменить</button>
            <button class="delete-message">Удалить</button>
            <button class="regenerate-message">Перегенерировать</button>
        </div>
    </div>`);
    $('#messages').scrollTop($('#messages')[0].scrollHeight);
        hljs.highlightAll();
    }

    function appendToLastMessage(content) {
        let lastMessageContainer = $('#messages .message-container').last();
        let lastMessage = lastMessageContainer.find('.message.bot');

        if (lastMessage.length === 0) {
            // Если нет сообщения от бота, создаем его
            let formattedContent = marked.parse(content);
            $('#messages').append(`<div class="message-container">
                <div class="message bot">${formattedContent}</div>
                <div class="message-actions">
                    <button class="edit-message">Изменить</button>
                    <button class="delete-message">Удалить</button>
                    <button class="regenerate-message">Перегенерировать</button>
                </div>
            </div>`);
        } else {
            // Если сообщение от бота уже есть, обновляем его содержимое
            // Берем текущий контент, добавляем новый, затем снова парсим через marked
            let currentContent = lastMessage.data('rawContent') || '';
            currentContent += content;
            lastMessage.data('rawContent', currentContent); // Сохраняем необработанный контент

            let formattedContent = marked.parse(currentContent);
            lastMessage.html(formattedContent);
        }

        $('#messages').scrollTop($('#messages')[0].scrollHeight);
        hljs.highlightAll();
    }

    function sendMessage() {
        const content = $('#message-input').val();
        if (content.trim() === '') return;
        $('#message-input').val('');
        if (currentChatId === null) {
            alert('Выберите или создайте чат.');
            return;
        }
        socket.emit('send_message', { chat_id: currentChatId, content: content });
    }

    // Обработчики кнопок управления сообщениями
    $('#messages').on('click', '.edit-message', function() {
        // Логика редактирования сообщения
    });

    $('#messages').on('click', '.delete-message', function() {
        // Логика удаления сообщения
    });

    $('#messages').on('click', '.regenerate-message', function() {
        const messageContainer = $(this).closest('.message-container');
        const messageId = messageContainer.data('message-id'); 
        socket.emit('regenerate_message', { chat_id: currentChatId, message_id: messageId });
        // Очистка последующих сообщений
        messageContainer.nextAll().remove();
    });
});

```


templates/index.html
```html

<!-- templates/index.html -->

<!DOCTYPE html>
<html lang="ru">
<head>
    <meta charset="UTF-8">
    <title>Chatbot</title>
    <link rel="stylesheet" href="{{ url_for('static', filename='css/styles.css') }}">
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.8.0/styles/default.min.css">
</head>
<body>
    <div class="container">
        <div class="sidebar">
            <button id="new-chat">Новый чат</button>
            <ul id="chat-list">
                <!-- Список чатов -->
            </ul>
        </div>
        <div class="chat-container">
            <div class="messages" id="messages">
                <!-- Сообщения чата -->
            </div>
            <div class="input-container">
                <input type="text" id="message-input" placeholder="Введите сообщение...">
            </div>
        </div>
    </div>

    <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.7.0/jquery.min.js"></script>
    <script src="//cdnjs.cloudflare.com/ajax/libs/socket.io/4.7.1/socket.io.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.8.0/highlight.min.js"></script>
    <script src="{{ url_for('static', filename='js/main.js') }}"></script>
</body>
</html>
```


Сейчас есть несколько проблем:
1) При генерации ответа в интерфейсе несколько первых символов обрезается
2) Кнопка удаления сообщения и прегенерации не работает. Также, не работает кнопка зименения сообщения.

Сделай, при наведении на кнопку чата, справа от него кнопку с иконкой ручки, чтобы можно было переименовать чат.